Навигация

Состояние и SEO в Nuxt 3: useState, Pinia, useHead и Open Graph

Любое приложение нуждается в управлении состоянием и в правильном отображении в поисковиках и социальных сетях. Nuxt 3 предоставляет для этого несколько инструментов: useState для разделяемого SSR-safe состояния, Pinia для сложного клиентского стейта, а useHead, useSeoMeta и definePageMeta — для управления <head> страницы.

В этой статье разберёмся, как выбирать между useState и Pinia, как настраивать SEO и Open Graph, как задавать meta-теги глобально и на уровне страницы, а также как режим рендеринга влияет на индексацию.

Содержание

  1. useState для разделяемого состояния
  2. Pinia в Nuxt 3
  3. useHead
  4. useSeoMeta
  5. definePageMeta
  6. Open Graph и Twitter Card
  7. Глобальные meta-теги в nuxt.config.ts
  8. SEO и режимы рендеринга
  9. Лучшие практики и антипаттерны
  10. Ключевые тезисы для интервью
  11. Заключение
  12. Полезные ссылки

useState для разделяемого состояния

useState — это встроенный composable Nuxt 3 для хранения разделяемого состояния, которое корректно работает при SSR. В отличие от обычного ref, useState требует уникальный ключ и сохраняет значение между сервером и клиентом в процессе гидратации.

Базовый пример

// composables/useSidebar.ts
export function useSidebar() {
  return useState('sidebar.open', () => false);
}
<!-- components/AppSidebar.vue -->
<script setup>
const isOpen = useSidebar();

function toggle() {
  isOpen.value = !isOpen.value;
}
</script>

Все вызовы useState('sidebar.open') в пределах одного приложения возвращают одну и ту же реактивную ссылку.

useState vs ref

useStateref
КлючОбязателенНет
Разделяемое состояниеДа, при одинаковом ключеНет
SSR-safeДа, сериализуется в payloadНет
ПрименениеГлобальное/разделяемое состояниеЛокальное состояние компонента

SSR-сериализация

При SSR значение useState попадает в глобальный объект __NUXT__ в HTML. Клиент использует это значение при гидратации, поэтому состояние не инициализируется дважды.

// composables/useCart.ts
export interface CartItem {
  id: number;
  name: string;
  price: number;
}

export function useCart() {
  const items = useState<CartItem[]>('cart.items', () => []);

  function add(item: CartItem) {
    items.value.push(item);
  }

  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price, 0)
  );

  return { items, add, total };
}

Когда использовать useState

  • Разделяемое UI-состояние: sidebar, модалки, тема.
  • Простое глобальное состояние, которое должно пережить гидратацию.
  • Данные, не требующие сложной логики, actions и DevTools.

Ограничения

  • useState не заменяет полноценный стейт-менеджер.
  • Нет встроенных actions, геттеров, плагинов и DevTools.
  • Для сложных сценариев лучше использовать Pinia.

Pinia в Nuxt 3

Pinia — официальная библиотека управления состоянием для Vue 3. В Nuxt 3 она подключается через модуль @pinia/nuxt, который автоматически регистрирует Pinia и добавляет удобные интеграции.

Установка и настройка

npm install @pinia/nuxt pinia
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@pinia/nuxt'],
});

Модуль @pinia/nuxt автоматически:

  • Создаёт и подключает экземпляр Pinia к приложению.
  • Автоимпортирует defineStore, storeToRefs, acceptHMRUpdate.
  • Автоимпортирует сторы из папки stores/.
  • Сериализует состояние Pinia при SSR и восстанавливает его на клиенте.

Базовый стор

// stores/cart.ts
export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [] as CartItem[],
  }),

  getters: {
    totalItems: (state) => state.items.length,
    totalPrice: (state) =>
      state.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
  },

  actions: {
    addItem(product: CartItem) {
      const existing = this.items.find((i) => i.id === product.id);
      if (existing) {
        existing.quantity++;
      } else {
        this.items.push({ ...product, quantity: 1 });
      }
    },

    removeItem(productId: number) {
      this.items = this.items.filter((i) => i.id !== productId);
    },
  },
});
<!-- pages/cart.vue -->
<script setup>
const cart = useCartStore();
const { items, totalPrice } = storeToRefs(cart);
</script>

Pinia vs useState

useStatePinia
СложностьПростое разделяемое состояниеПолноценный стейт-менеджер
Actions / gettersНетДа
Плагины / DevToolsНетДа
SSRСериализуется автоматическиСериализуется через @pinia/nuxt
Когда использоватьSidebar, тема, простые флагиКорзина, аутентификация, сложный UI

Для глубокого изучения Pinia смотрите отдельную статью.


useHead

useHead — composable для управления содержимым <head> документа. Он принимает объект с полями title, meta, link, script, style, htmlAttrs, bodyAttrs и поддерживает реактивные значения.

Базовый пример

<script setup>
useHead({
  title: 'Профиль пользователя',
  meta: [
    { name: 'description', content: 'Страница профиля пользователя' },
  ],
  link: [
    { rel: 'canonical', href: 'https://example.com/profile' },
  ],
});
</script>

Реактивные значения

Чтобы meta-теги зависели от реактивных данных, передайте функцию:

<script setup>
const title = ref('Главная страница');

useHead(() => ({
  title: title.value,
  meta: [
    { name: 'description', content: `Страница: ${title.value}` },
  ],
}));
</script>

Поддерживаемые поля

ПолеНазначение
titleЗаголовок страницы
titleTemplateШаблон заголовка, %s — заголовок страницы
metaМета-теги
linkСсылки: canonical, preload, stylesheet
scriptСкрипты
styleВстроенные стили
htmlAttrsАтрибуты <html>
bodyAttrsАтрибуты <body>
noscriptСодержимое <noscript>

Уникальность meta-тегов

Nuxt/Unhead автоматически дедуплицирует теги по name, property или charset. Чтобы принудительно обновить тег с тем же ключом, можно использовать key:

useHead({
  meta: [
    { name: 'description', content: 'Описание', key: 'description' },
  ],
});

useSeoMeta

useSeoMeta — удобный обёртка над useHead для SEO и Open Graph. Она предоставляет TypeScript-подсказки, автоматически генерирует нужные meta-теги и защищает от опечаток.

Базовый пример

<script setup>
useSeoMeta({
  title: 'Мой блог',
  description: 'Статьи о фронтенде и Nuxt',
  ogTitle: 'Мой блог',
  ogDescription: 'Статьи о фронтенде и Nuxt',
  ogImage: 'https://example.com/cover.jpg',
  ogUrl: 'https://example.com/blog',
  twitterCard: 'summary_large_image',
  twitterTitle: 'Мой блог',
  twitterDescription: 'Статьи о фронтенде и Nuxt',
  twitterImage: 'https://example.com/cover.jpg',
});
</script>

Этот код автоматически создаст:

<title>Мой блог</title>
<meta name="description" content="Статьи о фронтенде и Nuxt">
<meta property="og:title" content="Мой блог">
<meta property="og:description" content="Статьи о фронтенде и Nuxt">
<meta property="og:image" content="https://example.com/cover.jpg">
<meta property="og:url" content="https://example.com/blog">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Мой блог">
<meta name="twitter:description" content="Статьи о фронтенде и Nuxt">
<meta name="twitter:image" content="https://example.com/cover.jpg">

Реактивные SEO-мета

<script setup>
const post = await useFetch('/api/posts/1');

useSeoMeta(() => ({
  title: post.data.value?.title,
  description: post.data.value?.excerpt,
  ogTitle: post.data.value?.title,
  ogImage: post.data.value?.coverImage,
}));
</script>

Когда использовать useSeoMeta

  • Любые страницы, которые должны индексироваться.
  • Страницы с динамическим контентом: посты, товары, профили.
  • Когда важны социальные превью и Open Graph.

definePageMeta

definePageMeta — макрос для задания мета-информации страницы. Работает только в файлах pages/. Через него обычно настраивают layout, middleware, alias и другие параметры маршрута.

<script setup>
definePageMeta({
  layout: 'admin',
  middleware: 'auth',
});
</script>

Частые поля

ПолеНазначение
layoutИмя макета из layouts/
middlewareRoute middleware
aliasАльтернативный путь к странице
nameИмя маршрута
keepaliveНастройки <KeepAlive>
titleЗаголовок страницы (используется модулями SEO)

definePageMeta не управляет <head> напрямую, но многие SEO-модули читают его поля, например title, для генерации meta-тегов.


Open Graph и Twitter Card

Open Graph — протокол разметки страниц для социальных сетей (Facebook, LinkedIn, Telegram, Discord). Twitter Card — аналогичный протокол для Twitter/X. Хотя Twitter понимает og:*, явные twitter:* теги дают лучший контроль.

Основные Open Graph теги

ТегНазначение
og:titleЗаголовок для соцсетей
og:descriptionОписание
og:imageURL изображения превью
og:urlКанонический URL страницы
og:typeТип страницы: website, article
og:site_nameНазвание сайта
og:localeЛокаль: ru_RU, en_US

Основные Twitter Card теги

ТегНазначение
twitter:cardТип карточки: summary, summary_large_image
twitter:titleЗаголовок
twitter:descriptionОписание
twitter:imageИзображение
twitter:siteАккаунт сайта
twitter:creatorАвтор контента

Пример настройки превью

<script setup>
useSeoMeta({
  title: 'Как работает Nuxt 3',
  description: 'Разбираем SSR, SSG и гидратацию в Nuxt 3',
  ogTitle: 'Как работает Nuxt 3',
  ogDescription: 'Разбираем SSR, SSG и гидратацию в Nuxt 3',
  ogImage: 'https://example.com/nuxt-og.png',
  ogType: 'article',
  twitterCard: 'summary_large_image',
});
</script>

Проверка превью

Для проверки Open Graph и Twitter Card используйте:


Глобальные meta-теги в nuxt.config.ts

Для значений по умолчанию на всех страницах используйте app.head в nuxt.config.ts.

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      titleTemplate: '%s — My Nuxt App',
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
        { name: 'description', content: 'Мой сайт на Nuxt 3' },
        { property: 'og:site_name', content: 'My Nuxt App' },
      ],
      link: [
        { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
      ],
    },
  },
});

titleTemplate

%s заменяется на title текущей страницы:

// nuxt.config.ts
app: {
  head: {
    titleTemplate: '%s — My Nuxt App',
  },
}
<!-- pages/about.vue -->
<script setup>
useHead({ title: 'О нас' });
</script>

Результат: <title>О нас — My Nuxt App</title>.

Переопределение на уровне страницы

useHead и useSeoMeta на странице переопределяют глобальные значения. Если несколько компонентов задают один и тот же тег, побеждает последний по порядку рендера, но Nuxt дедуплицирует по ключам.


SEO и режимы рендеринга

Качество SEO напрямую зависит от того, как рендерится HTML. При SSR и SSG поисковые боты и социальные краулеры получают готовый HTML с заполненными meta-тегами. При чистом CSR HTML изначально пуст, и не все боты выполняют JavaScript.

Как рендеринг влияет на SEO

РежимHTML при первом запросеSEO
SSRПолныйОтлично
SSGПолныйОтлично
ISRПолный из кэшаХорошо
CSRПустой или минимальныйПлохо

Важные правила

  • useHead и useSeoMeta должны вызываться в <script setup> до или во время рендера, а не в обработчиках событий.
  • Данные для meta-тегов должны загружаться через useFetch / useAsyncData, чтобы попасть в серверный HTML.
  • Динамические маршруты с важным SEO-контентом должны быть доступны при nuxi generate или рендериться на сервере.

SSR-safe данные для SEO

<script setup>
const route = useRoute();

const { data: post } = await useFetch(`/api/posts/${route.params.slug}`);

useSeoMeta(() => ({
  title: post.value?.title,
  description: post.value?.description,
  ogTitle: post.value?.title,
  ogImage: post.value?.image,
}));
</script>

Такой код выполняется на сервере при SSR, и бот получает корректные meta-теги сразу в HTML.


Лучшие практики и антипаттерны

✅ Делайте

  • Используйте useState для простого разделяемого состояния, а Pinia — для сложного клиентского.
  • Задавайте уникальные ключи в useState, чтобы избежать коллизий.
  • Используйте useSeoMeta вместо ручного useHead({ meta: [...] }) для SEO.
  • Задавайте глобальные meta-теги и titleTemplate в nuxt.config.ts.
  • Делайте SEO-мета реактивными через функцию, если данные приходят из API.
  • Генерируйте og:url и canonical для каждой страницы.
  • Проверяйте социальные превью через официальные отладчики.
  • Тестируйте production-сборку: именно в ней виден финальный HTML.

❌ Не делайте

  • Не используйте useState для больших или сложных данных — это не замена Pinia.
  • Не инициализируйте useState значениями из localStorage или window — это вызовет hydration mismatch.
  • Не задавайте description и og:description статично, если контент динамический.
  • Не используйте useHead в обработчиках событий — meta-теги не попадут в SSR-HTML.
  • Не оставляйте страницы без title и description.
  • Не полагайтесь на CSR для страниц, которые должны индексироваться.

Ключевые тезисы для интервью

  • useState — SSR-safe разделяемое состояние с уникальным ключом; отличается от ref тем, что сериализуется в payload и разделяется между компонентами.
  • Pinia в Nuxt 3 подключается через @pinia/nuxt, автоматически импортирует сторы и сериализует состояние при SSR.
  • useState подходит для простого UI-состояния, Pinia — для сложного клиентского стейта: корзина, аутентификация, настройки.
  • useHead управляет <head> документа и поддерживает реактивные значения через функцию.
  • useSeoMeta — удобный composable для SEO и Open Graph с TypeScript-подсказками и автоматической генерацией meta-тегов.
  • definePageMeta задаёт мета-информацию страницы: layout, middleware, title; работает только в pages/.
  • Open Graph теги (og:*) нужны для социальных превью, Twitter Card (twitter:*) — для Twitter/X.
  • Глобальные meta-теги и titleTemplate задаются в app.head в nuxt.config.ts.
  • SEO эффективно только при SSR или SSG; CSR-страницы плохо индексируются.
  • Данные для SEO должны загружаться через useFetch / useAsyncData, чтобы попасть в серверный HTML.

Заключение

Управление состоянием и SEO — две стороны одной медали в full-stack фреймворке. Nuxt 3 даёт удобные инструменты для каждой задачи: useState для лёгкого SSR-safe стейта, Pinia для сложного клиентского управления, useHead и useSeoMeta для meta-тегов, а definePageMeta — для мета-информации страницы.

Правильный выбор между useState и Pinia, понимание SSR-сериализации и умение настраивать Open Graph позволяют строить быстрые и хорошо индексируемые приложения. Помните: красивый превью и правильные meta-теги работают только тогда, когда сервер отдаёт их в первоначальном HTML.

Попробуйте настроить app.head в nuxt.config.ts, добавьте useSeoMeta на несколько страниц и проверьте результат через отладчики социальных сетей.


Полезные ссылки

  • Nuxt 3 State Management — официальное руководство по управлению состоянием
  • Pinia Nuxt Module — интеграция Pinia в Nuxt
  • useHead — документация useHead
  • useSeoMeta — документация useSeoMeta
  • definePageMeta — документация definePageMeta
  • Unhead — движок управления head, лежащий в основе Nuxt
  • Open Graph Protocol — спецификация Open Graph