Как настроить SEO сайта на Vue

Google рендерит JavaScript, но делает это с задержкой и не всегда получает итоговый контент. Яндекс, соцсети и мессенджеры надежнее читают готовый HTML. Поэтому для контентного сайта выбирайте SSR или SSG, а CSR оставляйте закрытым разделам.

В статье вы выберете режим рендеринга, настроите Nuxt, метатеги, маршруты, sitemap и проверку страницы перед релизом.

Содержание

Почему сайт на Vue плохо индексируется: в чем отличие CSR от готового HTML

CSR собирает страницу в браузере после загрузки JavaScript. Первый ответ сервера часто содержит только контейнер приложения и ссылки на скрипты.

<div id="app"></div>
<script type="module" src="/assets/app.js"></script>

Робот должен скачать код, выполнить его, дождаться данных и получить итоговую разметку. Ошибка JavaScript, закрытый ресурс или медленный API могут оставить основной текст за пределами ответа.

SSR формирует HTML на сервере перед отправкой. SSG создает файлы во время сборки. В обоих случаях заголовок, текст, ссылки и метатеги попадают в исходный ответ.

Медленный внешний API опасен для SEO. Если запрос не завершится вовремя, поисковик может не увидеть описание товара или статьи. Отдавайте основной контент сервером, а интерактивность оставляйте клиентскому коду.

CSR хватает личному кабинету и закрытым разделам. Такие страницы зависят от учетной записи и не требуют индексации. Проверяйте доступ на сервере: клиентский роутер и robots.txt не защищают приватные данные.

Как Google, Яндекс и другие роботы обрабатывают JavaScript

Googlebot сначала получает HTML и ссылки. Затем робот может поставить страницу в очередь рендеринга и обработать JavaScript. Общие принципы описаны в документации Google по JavaScript SEO.

Возможность выполнить JavaScript не отменяет требования к доступности ресурсов. Ошибка приложения, запрос после действия пользователя или закрытый API оставят робота без текста.

Готовый HTML снижает зависимость страницы от возможностей конкретного робота. При SSR или SSG основной текст доступен уже в первом ответе сервера.

Другие роботы могут ограничиться первоначальным ответом. Для превью в соцсетях и мессенджерах формируйте Open Graph заранее.

Динамический рендеринг отдает ботам отдельную версию страницы. Google не рекомендует его как основное решение: команда поддерживает два варианта ответа и следит за совпадением содержания. SSR и SSG дают одну основу для робота и пользователя.

Режимы рендеринга Vue: CSR, SSR, SSG и пререндер

Выбирайте режим по открытости раздела, частоте обновлений и способу публикации. Один сайт может сочетать несколько вариантов.

ХарактеристикаCSRSSRSSGПререндер
Где формируется HTMLВ браузереНа сервере при запросеВо время сборкиЗаранее для выбранных адресов
Нагрузка на серверФайлы и APIРендеринг и APIРаздача файловРаздача готовых файлов
Свежесть данныхПосле клиентского запросаНа момент запроса, с учетом кешаНа момент сборкиНа момент подготовки файлов
Для каких сайтов подходитКабинеты и закрытые приложенияКаталоги и новостиБлоги и документацияНебольшой список публичных страниц
СложностьПростой старт, риски для SEOСервер, кеш и ошибкиМаршруты и пересборкаКонтроль адресов и файлов

Берите CSR для личного кабинета, если страницы закрыты от поиска. Используйте SSR для каталога с частыми изменениями. Выбирайте SSG для статей, документации и лендингов.

SSG использует предварительный рендеринг; эти понятия частично пересекаются. Пререндер выбранных маршрутов подходит существующему SPA с небольшим списком публичных адресов. Новый URL не появится в статическом HTML, пока сборка не получит его маршрут.

Как настроить SSR и SSG на Nuxt

Nuxt объединяет Vue, серверный рендеринг, файловые маршруты и статическую генерацию. Пример использует каталог app/ и проект Vue 3. Настройки рендеринга описаны в документации Nuxt.

Подготовьте минимальный проект

Для работы нужны Node.js и npm. Создайте пустой каталог проекта и откройте его в редакторе. Все команды примера выполняйте в терминале из этого каталога.

Создайте в корне файл package.json с командами проекта:

{
  "name": "vue-seo-demo",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "nuxt dev",
    "build": "nuxt build",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  }
}

Установите Nuxt и зависимости приложения:

npm install nuxt vue vue-router

После успешной установки появятся каталог node_modules и файл package-lock.json. npm добавит зависимости в package.json. Сохраните файл блокировки вместе с проектом, чтобы повторять установку зависимостей.

В корне создайте nuxt.config.ts:

export default defineNuxtConfig({
  srcDir: 'app/',
  pages: true,
  ssr: true
})

Создайте app/app.vue:

<template>
  <NuxtPage />
</template>

Добавьте главную страницу

Создайте app/pages/index.vue:

<template>
  <main>
    <h1>Руководства по Vue</h1>
    <p>Материалы о серверном рендеринге и статической генерации.</p>
    <NuxtLink to="/articles/ssr">Как работает SSR</NuxtLink>
  </main>
</template>

Запустите npm run dev и откройте адрес из терминала. В браузере появятся заголовок и абзац. В исходном коде страницы найдите тот же текст.

Как добавить динамические статьи в Nuxt

Динамический маршрут использует часть URL для выбора записи. Для примера подготовьте две статьи и общий компонент страницы.

Создайте app/data/articles.ts:

export const articles = [
  {
    slug: 'ssr',
    title: 'Как работает SSR',
    description: 'Серверный рендеринг страниц Vue.',
    text: 'Сервер формирует HTML до отправки ответа браузеру.'
  },
  {
    slug: 'ssg',
    title: 'Как работает SSG',
    description: 'Статическая генерация страниц Vue.',
    text: 'Сборщик сохраняет публичные страницы заранее.'
  }
]

Файл app/pages/articles/[slug].vue определит маршрут статьи:

<script setup lang="ts">
import { articles } from '~/data/articles'

const route = useRoute()
const article = computed(() =>
  articles.find(item => item.slug === route.params.slug)
)

if (!article.value) {
  throw createError({ statusCode: 404, statusMessage: 'Page not found' })
}
</script>

<template>
  <main v-if="article">
    <h1>{{ article.title }}</h1>
    <p>{{ article.text }}</p>
  </main>
</template>

Откройте /articles/ssr и обновите страницу. Текст должен остаться в исходном HTML. Прямой запрос к /articles/missing должен вернуть статус 404, который задает обработчик.

Число 404 задает HTTP-ошибку отсутствующей страницы. Один только текст «Страница не найдена» не меняет статус ответа.

Как собрать и проверить SSR, SSG и гибридные маршруты

Серверная сборка требует работающего приложения на хостинге. Статическая генерация сохраняет готовые файлы для раздачи без сервера Nuxt.

Соберите SSR-версию

Остановите режим разработки сочетанием Ctrl+C. Для сборки и локального предпросмотра выполните команды:

npm run build
npm run preview

Скопируйте адрес запущенного сервера из терминала. Добавьте к нему /articles/ssr и скачайте ответ командой curl -s. В HTML найдите заголовок и основной текст.

Соберите статические страницы

Перечислите главную страницу и динамические адреса в nuxt.config.ts:

import { articles } from './app/data/articles'

export default defineNuxtConfig({
  srcDir: 'app/',
  pages: true,
  ssr: true,
  nitro: {
    prerender: {
      routes: [
        '/',
        ...articles.map(article => `/articles/${article.slug}`)
      ]
    }
  }
})

Выполните npm run generate. В результате генерации найдите index.html и HTML-файлы обеих статей. Основной текст должен присутствовать в самих файлах.

Разделите маршруты по режимам

Для гибридного проекта сохраните список маршрутов и добавьте правила:

export default defineNuxtConfig({
  srcDir: 'app/',
  pages: true,
  ssr: true,
  nitro: {
    prerender: {
      routes: ['/', '/articles/ssr', '/articles/ssg']
    }
  },
  routeRules: {
    '/': { prerender: true },
    '/articles/**': { prerender: true },
    '/account/**': { ssr: false }
  }
})

Для гибридного развертывания выполните npm run build. Предпросмотр через npm run preview проверяет серверную сборку. Статический файловый сервер серверные маршруты не выполнит.

SSR без Nuxt: Vite SSR и vite-ssg

Vite SSR подходит проекту с собственным сервером или необычной схемой интеграции. Команда самостоятельно связывает серверную точку входа, маршруты, данные, head и статусы ответов.

vite-ssg генерирует статические страницы проекта на Vite. Берите этот вариант для документации и небольшого сайта с известным списком URL. Динамические маршруты получите до сборки.

Выбирайте Nuxt, если вам нужна готовая связка маршрутов, SSR и генерации. Vite SSR дает контроль над архитектурой, но переносит поддержку рендера и ошибок на команду.

Мета-теги, canonical и Open Graph для каждой страницы

Каждый публичный URL получает собственные title, description, canonical и Open Graph. Формируйте head во время SSR или SSG. Вызов внутри onMounted приходит после первого HTML.

Добавьте код в script setup динамической страницы:

const canonical = computed(() =>
  `https://example.com/articles/${article.value?.slug}`
)
const image = 'https://example.com/images/vue-seo-cover.png'

useSeoMeta({
  title: () => article.value?.title,
  description: () => article.value?.description,
  ogTitle: () => article.value?.title,
  ogDescription: () => article.value?.description,
  ogType: 'article',
  ogUrl: () => canonical.value,
  ogImage: image,
  twitterCard: 'summary_large_image'
})

useHead(() => ({
  link: [{ rel: 'canonical', href: canonical.value }]
}))

useSeoMeta задает SEO-поля, а useHead управляет остальными элементами head. Реактивные значения обновляют метаданные при смене маршрута.

  • Замените домен и путь изображения адресами своего проекта.
  • Задайте отдельный canonical для каждого основного URL.
  • Используйте абсолютные адреса страницы и изображения.
  • Сделайте изображение Open Graph доступным без авторизации.
  • Согласуйте canonical с редиректами и протоколом сайта.

Скачайте исходный HTML двух страниц. У них должны различаться title, description, canonical и og:url. Затем перейдите между статьями без перезагрузки и проверьте обновление head.

Маршруты и динамические страницы: history mode, 404 и ссылки

Индексируемый адрес должен открываться напрямую, возвращать подходящий HTTP-статус и содержать собственный HTML. Вложенные и динамические страницы требуют отдельной проверки.

Включите history mode

В обычном Vue Router используйте createWebHistory. Фрагмент после # сервер не получает, поэтому hash-маршруты неудобны для отдельных индексируемых страниц.

import { createRouter, createWebHistory } from 'vue-router'
import ArticlePage from './pages/ArticlePage.vue'

export const router = createRouter({
  history: createWebHistory(),
  routes: [{ path: '/articles/:slug', component: ArticlePage }]
})

Nuxt создает маршруты по файлам страниц. Не подключайте второй роутер к Nuxt-проекту.

Настройте fallback и настоящий 404

Клиентскому SPA нужен fallback на index.html, чтобы прямой запрос к существующему маршруту дошел до приложения. Для Nginx правило внутри серверного блока выглядит так:

location / {
    try_files $uri $uri/ /index.html;
}

Такой fallback возвращает стартовый HTML даже для неизвестного пути. Не переносите его без изменений на SSR или SSG. Для SSR передавайте запрос серверу Nuxt и выбрасывайте createError со статусом 404.

Для статики сервер должен отдавать существующие файлы и возвращать 404 для отсутствующих. Проверьте опубликованный неизвестный адрес командой curl -I.

Оставьте ссылки доступными роботу

Для переходов используйте ссылку или компонент маршрутизатора:

<a href="/articles/ssg">Статическая генерация</a>
<NuxtLink to="/articles/ssg">Статическая генерация</NuxtLink>
<RouterLink to="/articles/ssg">Статическая генерация</RouterLink>

В Nuxt используйте NuxtLink, в обычном Vue Router – RouterLink. Кнопка с @click и вызовом router.push не заменяет ссылку. Робот должен найти URL в HTML без нажатия кнопки.

Sitemap и robots.txt для сайта на Vue

Sitemap помогает обнаруживать опубликованные адреса, включая динамические страницы. Robots.txt задает правила сканирования. Ни один файл не гарантирует индексацию.

Для небольшого проекта подготовьте XML вместе со статическими ресурсами. Создайте в корне проекта файл make-sitemap.mjs:

import { writeFileSync, mkdirSync } from 'node:fs'

const origin = 'https://example.com'
const slugs = ['ssr', 'ssg']
const paths = ['/', ...slugs.map(slug => `/articles/${slug}`)]
const escapeXml = value => value
  .replaceAll('&', '&amp;')
  .replaceAll('<', '&lt;')
  .replaceAll('>', '&gt;')
  .replaceAll('"', '&quot;')
  .replaceAll("'", '&apos;')

const entries = paths.map(path => {
  const url = new URL(path, origin).href
  return `<url><loc>${escapeXml(url)}</loc></url>`
}).join('')

const xml = '<?xml version="1.0" encoding="UTF-8"?>'
  + '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
  + entries + '</urlset>'

mkdirSync('public', { recursive: true })
writeFileSync('public/sitemap.xml', xml, 'utf8')

Замените https://example.com на основной адрес проекта. Массив slugs должен содержать все опубликованные статьи. Для данных из CMS получите полный список записей перед формированием paths.

Из корня проекта запустите node make-sitemap.mjs. В файле public/sitemap.xml должны появиться главная страница и статьи.

Создайте public/robots.txt:

User-agent: *
Allow: /
Disallow: /account/

Sitemap: https://example.com/sitemap.xml

После публикации откройте /robots.txt и /sitemap.xml. Оба адреса должны вернуть свои файлы со статусом 200. Не включайте в sitemap закрытые, ошибочные, неканонические страницы и редиректы.

Скорость и Core Web Vitals на Vue

SSR передает готовый текст, но браузер еще гидратирует приложение. Во время гидратации Vue связывает серверную разметку с клиентскими компонентами.

  • Разделяйте JavaScript по маршрутам.
  • Загружайте тяжелые виджеты после основного содержимого.
  • Применяйте lazy-loading к изображениям вне первого экрана.
  • Не откладывайте изображение, которое формирует LCP.
  • Задавайте размеры изображений и предотвращайте сдвиги макета.

LCP описывает появление крупнейшего элемента, INP – отзывчивость интерфейса, CLS – сдвиги макета. Большой JavaScript-пакет может задержать взаимодействие после появления HTML.

Для локальной диагностики используйте Lighthouse в инструментах разработчика Chrome. Откройте опубликованную страницу, затем инструменты разработчика и панель Lighthouse. Выберите анализ производительности и запустите проверку. Отчет покажет метрики загрузки и рекомендации по сокращению JavaScript.

Лабораторный отчет не заменяет измерение Core Web Vitals у реальных посетителей.

Получайте основной SEO-текст на сервере через useFetch:

const route = useRoute()
const { data: article, error } = await useFetch(
  () => `/api/articles/${encodeURIComponent(String(route.params.slug))}`
)

if (error.value) {
  throw createError({
    statusCode: error.value.statusCode === 404 ? 404 : 503,
    statusMessage: 'Content unavailable'
  })
}

if (!article.value) {
  throw createError({ statusCode: 404, statusMessage: 'Page not found' })
}

Запрос внутри onMounted выполняется только в браузере и не наполнит серверный HTML. При отказе API не отдавайте пустую статью со статусом 200.

Как проверить, что страница проиндексируется

Техническая проверка убирает препятствия, но не гарантирует попадание страницы в индекс. Проверяйте опубликованный адрес без авторизации.

Сравните исходный HTML и DOM

  1. Откройте вложенный URL напрямую и обновите страницу. Она должна загрузиться без перехода через главную.
  2. Выполните curl -s https://example.com/articles/ssr, заменив домен адресом проекта. В ответе найдите заголовок и основной текст.
  3. Проверьте title, description, canonical и og:image в том же ответе.
  4. Выполните curl -I https://example.com/articles/ssr. Публичная страница должна вернуть 200.
  5. Запросите неизвестный slug. Для него нужен статус 404.
  6. Отключите JavaScript и перезагрузите страницу. Основной текст и ссылки должны остаться доступными.

Панель Elements показывает DOM после выполнения JavaScript. Для SSR смотрите исходный ответ через curl или просмотр исходного кода.

Используйте Google Search Console и Яндекс.Вебмастер

Откройте официальное руководство Google Search Console и перейдите из него в сервис. Выберите свой сайт и вставьте полный адрес в поле проверки URL. Запустите проверку опубликованной страницы. В результатах найдите просмотр HTML и сравните текст с исходным ответом сервера.

В Яндекс.Вебмастере используйте инструмент проверки ответа сервера для своего сайта. Введите существующий адрес, затем неизвестный URL. Сравните HTTP-статусы: опубликованная страница должна вернуть 200, отсутствующая – 404.

Заголовки ответа и исходный HTML можно сохранить локально одной командой:

curl -sS -D headers.txt https://example.com/articles/ssr -o page.html

Пройдите чек-лист перед релизом

  • Основной текст и ссылки есть в исходном HTML.
  • Разные страницы получают разные title, description и canonical.
  • Open Graph содержит абсолютные адреса страницы и изображения.
  • Прямые переходы и обновление вложенных страниц работают.
  • Публичные страницы возвращают 200, отсутствующие – 404.
  • В проекте нет случайного noindex или запрета сканирования.
  • Sitemap включает все опубликованные динамические страницы.
  • Robots.txt доступен и указывает sitemap.
  • Скрипты, стили и публичные данные доступны без сессии пользователя.
  • Ошибка API не превращает страницу в пустой ответ 200.
  • Тяжелые виджеты не задерживают основной контент.
  • Клиентские ошибки проверены локально, серверные сбои видны в журнале.

Как диагностировать ошибки рендеринга локально

Клиентский сбой может оставить пользователя без контента после успешного SSR. Для локальной диагностики выводите ошибки Vue, необработанные исключения и отклоненные Promise в консоль. Пример работает только в открытом браузере и не собирает ошибки посетителей на сервере.

Создайте файл app/plugins/error-monitor.client.ts. Суффикс .client.ts исключает выполнение этого кода при SSR.

export default defineNuxtPlugin(nuxtApp => {
  function report(kind: string, message: string) {
    console.error('[render-error]', { kind, message })
  }

  window.addEventListener('error', event => {
    report('error', event.message)
  })

  window.addEventListener('unhandledrejection', event => {
    const reason = event.reason
    report('unhandledrejection',
      reason instanceof Error ? reason.message : String(reason))
  })

  nuxtApp.hook('vue:error', error => {
    report('vue:error', error instanceof Error ? error.message : String(error))
  })
})

Для проверки создайте временную страницу app/pages/render-test.vue:

<script setup lang="ts">
function triggerError() {
  throw new Error('render-test')
}
</script>

<template>
  <main>
    <button type="button" @click="triggerError">
      Вызвать тестовую ошибку
    </button>
  </main>
</template>
  1. Запустите npm run dev из корня проекта. Откройте адрес из терминала с путем /render-test. На странице появится кнопка «Вызвать тестовую ошибку».
  2. Откройте инструменты разработчика браузера и перейдите в консоль. Включите отображение ошибок, если этот уровень скрыт фильтром.
  3. Нажмите кнопку «Вызвать тестовую ошибку». В консоли появится запись [render-error] с полями kind: 'vue:error' и message: 'render-test'.

После проверки удалите app/pages/render-test.vue, чтобы тестовый маршрут не попал в публикацию. Чтобы убрать диагностические обработчики, удалите app/plugins/error-monitor.client.ts и перезапустите приложение.

Этот плагин помогает воспроизвести сбой при ручном тестировании. Он не настраивает производственный мониторинг, хранение событий или уведомления команде.

Проверьте также отказ API на тестовом окружении. Серверный журнал должен содержать причину сбоя, а публичный ответ – подходящий статус ошибки.

Ошибки SEO на сайтах Vue

Проверяйте внешний вид страницы, HTML-ответ, ссылки и статус каждого публичного адреса.

ОшибкаПоследствиеИсправление
Hash-маршрутыСервер не получает отдельный путьВключить history mode и настроить прямые запросы
Мета-теги только на клиентеРобот получает общий headФормировать head через SSR или SSG
Текст появляется после прокруткиРобот может не запустить загрузкуОтдать основной текст сразу в HTML
Переход через один @clickВ HTML нет обнаруживаемой ссылкиИспользовать a href, RouterLink или NuxtLink
Ошибка API не обработанаСтраница теряет контентВернуть ошибку или рабочую сохраненную версию
SSG знает не все slugЧасть страниц отсутствует после публикацииПолучать полный список маршрутов перед сборкой

Анимация сама по себе не запрещает индексацию. Проблема возникает, когда текст появляется только после действия пользователя или задержанного запроса. Оставляйте содержание в HTML, а эффекты добавляйте поверх него.

Какой режим выбрать для проекта на Vue

Для блога, документации и лендинга выбирайте SSG. После публикации запускайте новую сборку. Для каталога с частыми изменениями используйте SSR и кеширование.

Гибридная схема подходит большому проекту: статьи генерируются заранее, каталог рендерится на сервере, а кабинет работает в CSR. Для каждого типа страницы задайте правила и проверьте head, ссылки и статусы.

SSR Vue и SSG доставляют HTML, но не заменяют полезный текст, доступные маршруты и обработку ошибок. После каждого релиза скачивайте исходный HTML вложенной страницы.

Совет от нашей редакции: сохраните HTML рабочей страницы перед релизом и сравните его с новой сборкой. Так вы заметите исчезновение основного текста до публикации.

Комментарии

Оставьте свой комментарий
Пока никто не оставил комментариев