Другое

Получение SEO‑данных в Nuxt.js SSR: Полное руководство

Узнайте, как получать SEO‑данные из внешних API в Nuxt.js SSR, решать проблемы гидратации и обеспечивать доступность данных при серверном рендеринге для улучшения SEO и повышения производительности вашего приложения.

Как получить данные SEO из внешнего API (Strapi) в приложении Nuxt.js и сделать их доступными во время сервер‑сайд рендеринга? Я использую useState для хранения SEO‑данных, но они становятся доступными только после гидратации, что вызывает проблемы при попытке доступа к ним на клиенте. Вот моя текущая реализация:

javascript
import { getEnv } from "../src/utils";
import { useState } from "#app";

export default defineNuxtPlugin(async (nuxtApp) => {
  const config = getEnv();
  const currentPath = nuxtApp.ssrContext?.event.node.req.url || "unknown-path";
  const lang = currentPath.split("/")[1];

  try {
    const { data, error } = await useAsyncData(
      "mediaPagina-locales",
      () =>
        $fetch(`${config.API_URL}/api/pages?locale=${lang}&populate=*`, {
          method: "GET",
        }),
      {
        server: true,
        lazy: false,
        default: () => null,
        stale: 60 * 1000 * 60 * config.LOCAL_CACHE_HOURS,
      }
    );

    if (error.value) {
      console.warn("⚠️ Error:", error.value);
      return;
    }

    if (!data.value?.data) {
      console.warn("⚠️ Error.");
      return;
    }

    const pages = data.value.data;
    console.log("✅ Error", pages.length, "entries");

    useState("cookies_locales", () => pages.filter((i: any) => i?.page === "cookies"));
    useState("cookies_seo_locales", () => pages.filter((i: any) => i?.page === "cookies/seo"));
  } catch (err) {
    console.error("❌ Error fetchPageData:", err);
  }
});

В моём фронтенд‑компоненте:

javascript
const contactSeo = useState('cookies_seo_locales')
const seo = computed(() => unref(cookies)[0]);

Такой подход не работает, потому что переменная доступна только после гидратации. Как сделать так, чтобы данные SEO были доступны во время сервер‑сайд рендеринга и гидратации?

Когда вы работаете с внешними данными API в приложениях Nuxt.js с SSR, ключевым моментом является понимание того, как работает гидратация, и использование правильных composable‑функций, чтобы данные были доступны как на сервере, так и во время гидратации клиента. Ваша текущая реализация использует useState после получения данных с помощью useAsyncData, но проблема в том, что useState создаёт состояние, которое доступно только после гидратации компонента на клиенте.

Содержание


Понимание проблемы гидратации

Несоответствие гидратации, которое вы наблюдаете, возникает из‑за того, что useState создаёт реактивное состояние, которое доступно только на клиенте после того, как начальный HTML загружен и Vue берёт управление. При серверном рендеринге сервер генерирует HTML, но клиенту необходимо «гидратировать» этот HTML, привязывая реактивную систему Vue и повторно выполняя тот же код, который генерировал серверный контент.

Согласно руководству LogRocket о управлении состоянием Nuxt, useState предназначен для предоставления реактивного и постоянного состояния между компонентами и запросами, но имеет специфическое поведение во время гидратации, которое нужно понимать.


Правильная загрузка данных SSR в Nuxt

Для SEO‑важных данных, которые должны быть доступны во время серверного рендеринга, Nuxt предоставляет несколько composable‑функций:

Правильное использование useAsyncData

Ваша реализация useAsyncData в целом правильна, но необходимо убедиться, что данные корректно передаются клиенту. Composable useAsyncData автоматически обрабатывает сериализацию данных и их передачу между сервером и клиентом.

javascript
const { data, error } = await useAsyncData(
  "mediaPagina-locales",
  () =>
    $fetch(`${config.API_URL}/api/pages?locale=${lang}&populate=*`, {
      method: "GET",
    }),
  {
    server: true,
    lazy: false,
    default: () => null,
    transform: (data) => data?.data || null,
  }
);

Использование useFetch для простоты

Для загрузки данных API часто рекомендуется использовать useFetch, поскольку он автоматически обрабатывает вопросы SSR:

javascript
const { data } = await useFetch(`${config.API_URL}/api/pages`, {
  key: `mediaPagina-locales-${lang}`,
  query: { locale: lang, populate: '*' },
  server: true,
  lazy: false,
  default: () => [],
});

Использование useState правильно для SEO‑данных

Чтобы сделать SEO‑данные доступными как при SSR, так и во время гидратации, вы должны инициализировать useState начальными данными, полученными из вашего запроса:

javascript
export default defineNuxtPlugin(async (nuxtApp) => {
  const config = getEnv();
  const currentPath = nuxtApp.ssrContext?.event.node.req.url || "unknown-path";
  const lang = currentPath.split("/")[1];

  // Сначала получаем данные
  const { data, error } = await useAsyncData(
    "mediaPagina-locales",
    () =>
      $fetch(`${config.API_URL}/api/pages?locale=${lang}&populate=*`, {
        method: "GET",
      }),
    {
      server: true,
      lazy: false,
      default: () => null,
      transform: (data) => data?.data || null,
    }
  );

  if (error.value || !data.value) {
    console.warn("⚠️ Ошибка при получении SEO‑данных:", error.value);
    return;
  }

  // Создаём реактивное состояние с полученными данными
  const cookiesPages = useState("cookies_locales", () => 
    data.value.filter((i: any) => i?.page === "cookies")
  );
  
  const cookiesSeoPages = useState("cookies_seo_locales", () => 
    data.value.filter((i: any) => i?.page === "cookies/seo")
  );
});

В вашем компоненте вы можете затем получить доступ к этим данным напрямую:

javascript
const contactSeo = useState('cookies_seo_locales');
const seo = computed(() => contactSeo.value?.[0] || null);

Альтернативные решения с useHydration

Для более сложных сценариев управления состоянием можно использовать composable useHydration:

javascript
export default defineNuxtPlugin((nuxtApp) => {
  const config = getEnv();
  const currentPath = nuxtApp.ssrContext?.event.node.req.url || "unknown-path";
  const lang = currentPath.split("/")[1];

  // Плагин для получения и гидратации SEO‑данных
  nuxtApp.hooks.hook('app:rendered', async () => {
    try {
      const response = await $fetch(`${config.API_URL}/api/pages`, {
        query: { locale: lang, populate: '*' },
      });
      
      useHydration('seoData', () => response?.data || []);
    } catch (error) {
      console.error('Не удалось получить SEO‑данные:', error);
    }
  });
});

Лучшие практики работы с внешними API

1. Стратегическое кэширование

javascript
const { data } = await useFetch(`${config.API_URL}/api/pages`, {
  key: `seo-pages-${lang}`,
  query: { locale: lang, populate: '*' },
  server: true,
  default: () => [],
  cache: true, // Включить кэширование
  watch: false, // Отключить автоматический рефетч
});

2. Обработка состояний загрузки

javascript
const { data, pending, error } = await useFetch('/api/pages', {
  query: { locale: lang, populate: '*' },
  server: true,
  default: () => [],
});

// В компоненте
if (pending.value) {
  return 'Загрузка SEO‑данных...';
}
if (error.value) {
  return 'Ошибка при загрузке SEO‑данных';
}

3. Использование типовой безопасности

javascript
interface SeoPage {
  id: number;
  page: string;
  attributes: {
    title?: string;
    description?: string;
    meta?: any;
  };
}

const { data } = await useFetch<SeoPage[]>('/api/pages', {
  query: { locale: lang, populate: '*' },
  server: true,
  default: () => [],
});

Полный пример реализации

Ниже приведено полностью рабочее решение:

javascript
// plugins/seo-data.server.ts
import { getEnv } from "../src/utils";

export default defineNuxtPlugin(async (nuxtApp) => {
  const config = getEnv();
  const currentPath = nuxtApp.ssrContext?.event.node.req.url || "unknown-path";
  const lang = currentPath.split("/")[1];

  try {
    // Используем useFetch для автоматической обработки SSR
    const { data, error } = await useFetch('/api/pages', {
      key: `seo-pages-${lang}`,
      query: { locale: lang, populate: '*' },
      server: true,
      lazy: false,
      default: () => [],
      transform: (data) => data?.data || [],
    });

    if (error.value) {
      console.warn("⚠️ Ошибка при получении SEO‑данных:", error.value);
      return;
    }

    // Создаём реактивное состояние из полученных данных
    const allPages = data.value;
    
    useState("cookies_locales", () => 
      allPages.filter((page: any) => page?.page === "cookies")
    );
    
    useState("cookies_seo_locales", () => 
      allPages.filter((page: any) => page?.page === "cookies/seo")
    );
    
    // Общее состояние страниц
    useState("seo_pages", () => allPages);

  } catch (err) {
    console.error("❌ Ошибка в плагине SEO‑данных:", err);
  }
});

В ваших компонентах:

javascript
// components/SeoComponent.vue
<script setup>
const cookiesSeo = useState('cookies_seo_locales');
const seoPages = useState('seo_pages');

// Это будет работать как при SSR, так и во время гидратации
const currentSeo = computed(() => {
  return cookiesSeo.value?.[0] || null;
});
</script>

Устранение распространённых проблем

Ошибки несоответствия гидратации

Если вы всё ещё видите ошибки гидратации, убедитесь, что:

  1. Значения по умолчанию: всегда предоставляйте значения по умолчанию для вашего состояния
  2. Постоянные данные: сервер и клиент должны получать идентичные данные
  3. Избегайте клиентских API: не используйте браузерные API в контексте SSR
javascript
// Вместо этого (вызывает несоответствие гидратации)
const seoData = useState('seo_data');
const title = seoData.value?.title;

// Используйте такой паттерн
const seoData = useState('seo_data', () => null); // всегда задавайте значение по умолчанию
const title = computed(() => seoData.value?.title || '');

Данные не обновляются

Если ваши данные не обновляются должным образом:

javascript
// Используйте watchEffect для рефетча при необходимости
const { data, refresh } = await useFetch('/api/pages', {
  query: { locale: lang, populate: '*' },
  server: true,
  default: () => [],
});

watchEffect(() => {
  // Обновляем состояние, когда данные меняются
  useState('seo_pages', () => data.value);
});

Оптимизация производительности

Для лучшей производительности с внешними API:

javascript
// Используйте паттерн SWR (Stale-While-Revalidate)
const { data, error } = await useFetch('/api/pages', {
  query: { locale: lang, populate: '*' },
  key: `seo-${lang}`,
  server: true,
  default: () => [],
  cache: true,
  watch: false, // Отключить авто‑рефетч
});

// Ручной рефетч при необходимости
const refreshSeoData = () => {
  refreshNuxtData('seo-pages');
};

Источники

  1. Nuxt state management and hydration with useState - LogRocket Blog
  2. Understanding Hydration in Server-Side Rendered Nuxt Applications | Medium
  3. useHydration · Nuxt Composables v4
  4. A Comprehensive Guide to Data Fetching in Nuxt 3 - Michael Hoffmann
  5. How To Use Server-Side Rendering with Nuxt.js | DigitalOcean
  6. Vue to Nuxt: Server-Side Rendering Example | Alokai

Заключение

Чтобы правильно получать и обслуживать SEO‑данные из внешних API в приложениях Nuxt.js с SSR:

  1. Используйте useFetch или useAsyncData для первоначальной загрузки данных с правильной конфигурацией SSR
  2. Инициализируйте useState с полученными данными для создания реактивного состояния, доступного во время гидратации
  3. Предоставляйте значения по умолчанию для предотвращения несоответствий гидратации
  4. Рассмотрите стратегии кэширования для оптимизации производительности
  5. Используйте надёжную обработку ошибок для плавного отката

Ключевой вывод: useState должен быть инициализирован данными, которые уже доступны из вашего SSR‑запроса, а не создавать новое состояние после того, как данные уже получены. Это гарантирует, что ваши SEO‑данные будут доступны как во время серверного рендеринга, так и во время клиентской гидратации, решая исходную проблему.

Авторы
Проверено модерацией
Модерация