SEO для Nuxt: полный гайд по индексации и метатегам

Если ты сделал сайт на Vue и не видишь его в поиске, дело почти наверняка в рендере, и SEO для Nuxt начинается именно отсюда. Чистый Vue отдаёт браузеру пустой каркас, а контент подрисовывает JavaScript уже потом. Поисковый бот приходит на такую страницу и видит почти ничего. Nuxt эту проблему решает в корне: он рендерит HTML на сервере или заранее на сборке, и краулер получает готовую страницу с текстом, заголовками и метатегами. В этом гайде я разберу полный цикл, от первого useSeoMeta до проверки в Google Search Console, с кодом и граблями, на которые сам наступал.
Меня зовут Евгений Волков, я fullstack-разработчик, и evolkov.tech, где ты сейчас читаешь эту статью, сам работает на Nuxt в режиме SSR-сервера. То есть про SEO для Nuxt я пишу не по докам, а из своей рабочей практики: настраивал индексацию и на визитках, и на международной контентной платформе на 1000+ страниц. Русскоязычного полного гайда по теме в топе почти нет, всё англоязычное и раздроблено по кусочкам. Соберу целую картину в одном месте. Год на дворе 2026, все примеры под актуальный Nuxt.
Почему у Nuxt-сайта индексация решается серверным рендерингом
Разберём по шагам, что видит краулер, потому что без этого остальное не имеет смысла.
Открой любой SPA-сайт на чистом Vue и посмотри исходный код страницы через «Просмотр кода» в браузере, не DevTools, а именно raw-исходник. Там будет пусто: <div id="app"></div> и подключённые скрипты. Весь текст, заголовки, ссылки появляются позже, когда браузер скачает и выполнит JavaScript. Человек этого не замечает, потому что у него мощный браузер. А поисковый бот сначала видит ровно этот пустой каркас.
Google формально умеет выполнять JavaScript и рендерить SPA. Но делает это в две волны: сначала обходит HTML, а рендеринг с выполнением JS откладывает на потом, когда дойдут руки и краулинговый бюджет. Это медленнее, дороже и капризнее. На больших сайтах часть страниц может месяцами висеть «обнаруженными, но не проиндексированными». А Яндекс с JavaScript исторически дружит хуже Google, и для рунета это критично.
SSR и prerender убирают эту развилку целиком. Сервер (или билд) сам выполняет Vue-приложение и отдаёт краулеру уже готовый HTML: с текстом, заголовками h1-h6, ссылками, метатегами. Боту нечего дорисовывать, он индексирует страницу с первого захода. Вот и вся суть. Nuxt берёт удобство Vue-компонентов и добавляет к нему серверную отрисовку, из-за которой сайт становится видимым поиску.
+Плюсы
- SSR / prerender (Nuxt): краулер получает готовый HTML сразу, индексация быстрая и предсказуемая, Яндекс видит контент без выполнения JS.
- Метатеги, canonical, structured data приезжают в исходном HTML, а не дорисовываются скриптом.
- Первый экран рисуется раньше, LCP лучше, поведенческие сигналы выше.
−Минусы
- Чистый SPA (Vue без Nuxt): в исходном HTML контента нет, бот должен выполнить весь JS.
- Рендеринг откладывается на вторую волну, часть страниц зависает вне индекса.
- Яндекс индексирует SPA нестабильно, для рунета это прямая потеря видимости.
Какой режим рендера выбрать: SSR, prerender или гибрид
Первое большое решение по SEO принимается до единой строчки метатегов. Оно про то, как вообще браузер и бот получат страницу.
SSR
Сервер на каждый запрос собирает готовый HTML и отдаёт его. Контент всегда свежий, персонализация работает, краулер видит полную страницу. Плата: нужен живой Node-сервер, TTFB зависит от его скорости, под нагрузкой нужен кеш. Это режим самого evolkov.tech, потому что на сайте есть контактная форма, которая шлёт запрос на серверный роут. SSR берут, когда контент завязан на пользователя или меняется постоянно.
SSG (prerender)
Страницы собираются один раз на билде в статические HTML-файлы. Дальше их раздаёт хоть простой файловый сервер, хоть CDN. Для SEO это идеал: TTFB почти нулевой, бот получает готовый HTML мгновенно, ломаться на рантайме нечему. Плата: при изменении контента нужна пересборка. Для блога, лендинга, витрины услуг, портфолио это оптимальный выбор. Большинство сайтов, которым важно SEO, живут именно на prerender.
Гибрид через routeRules
Часто правильный ответ не «или-или», а смесь. Nuxt даёт для этого routeRules прямо в конфиге: для каждого участка сайта задаёшь свою стратегию. Витрину и блог префендерим в статику, динамику отдаём SSR, а редко меняющиеся страницы кешируем через swr. Служебное закрываем от индексации.
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Главная и услуги: статика на билде, самый быстрый вариант для SEO
'/': { prerender: true },
'/uslugi/**': { prerender: true },
// Блог: статика, но пересобираем раз в час без полного билда
'/blog/**': { swr: 3600 },
// Личный кабинет: только SSR, свежие данные под пользователя
'/account/**': { ssr: true },
// Служебное: прячем от поиска на уровне заголовка
'/admin/**': { robots: false },
},
})
Одна деталь, на которой спотыкаются. swr (stale-while-revalidate) отдаёт закешированную страницу мгновенно и тихо пересобирает её в фоне. Это компромисс между свежестью SSR и скоростью статики, и для контента, который меняется не ежесекундно, он часто лучше обоих крайних вариантов.
Метатеги: useSeoMeta и useHead
Разобрались с рендером, теперь то, ради чего половина людей читает такой гайд: title, description и прочие метатеги. В Nuxt для этого два основных инструмента, и путаница между ними частая.
useSeoMeta это типизированный помощник специально под SEO-теги: title, description, Open Graph, Twitter. Пишешь плоский объект, а он сам раскладывает его в правильные <meta>. useHead шире и ниже уровнем: им управляешь любыми тегами головы, включая <link>, <script>, <html lang>. Правило простое: для обычных SEO-мета бери useSeoMeta, для canonical, hreflang и скриптов schema.org бери useHead.
Дефолты в nuxt.config
Сначала задаём базу для всего сайта в конфиге, чтобы ни одна страница не осталась совсем без метатегов.
// nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
htmlAttrs: { lang: 'ru' },
titleTemplate: '%s · evolkov.tech',
meta: [
{ name: 'description', content: 'Fullstack-разработка на Nuxt и Vue: сайты, веб-приложения, техническое SEO.' },
{ property: 'og:site_name', content: 'evolkov.tech' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
],
},
},
})
Динамические метатеги на страницу
Внутри страницы или лейаута перебиваем дефолты своими. Вот тут живёт главная грабля динамических роутов, к ней вернусь в разделе про проверку. Ключевой момент: если title или description зависят от данных, которые грузятся асинхронно, передавай их как функции-геттеры, а не как готовые значения. Иначе метатеги застынут на первом рендере и на карточке товара будет title соседней.
<script setup lang="ts">
const { data: post } = await useAsyncData('post', () => queryContent(route.path).findOne())
// Геттеры, а не голые строки: так теги пересоберутся, когда данные приедут
useSeoMeta({
title: () => post.value?.metaTitle ?? post.value?.title,
description: () => post.value?.metaDescription ?? post.value?.excerpt,
ogTitle: () => post.value?.title,
ogDescription: () => post.value?.excerpt,
ogType: 'article',
})
</script>
Шаблон title
titleTemplate из конфига выше добавляет суффикс · evolkov.tech к каждому title автоматически. Но помни про правило проекта: meta.title должен отличаться от h1, а не быть «h1 плюс бренд». На главной или на важной посадочной я иногда отключаю суффикс, вернув null, чтобы title читался как отдельная законченная формулировка под ключевой запрос, а не механически склеенный хвост.
Open Graph и превью в соцсетях
Когда ссылку на твой сайт кидают в Telegram, ВКонтакте или мессенджер, разворачивается карточка: заголовок, описание, картинка. Это Open Graph. Плохое превью режет кликабельность, и это тоже часть SEO в широком смысле, потому что репосты дают трафик и сигналы.
Базовые og-теги ты уже видел в примере с useSeoMeta: ogTitle, ogDescription, ogType. Отдельно важна ogImage, картинка превью. Можно повесить статичную на весь сайт, а можно генерить уникальную под каждую страницу автоматически. Для второго есть модуль nuxt-og-image: ты описываешь шаблон картинки как обычный Vue-компонент, а модуль на билде или на лету рендерит из него PNG с заголовком статьи, автором, лого.
// На странице статьи: сгенерировать OG-картинку из шаблона
defineOgImageComponent('BlogPost', {
title: post.value?.title,
author: 'Евгений Волков',
})
Модуль сам подставит в <meta property="og:image"> ссылку на сгенерированную картинку нужного размера (1200×630). Уникальное превью под каждый пост заметно поднимает CTR в соцсетях, а руками рисовать их под сотню статей никто не станет. Тут автоматизация окупается сразу.
Canonical: убираем дубли
Дубли это тихий убийца SEO. Одна и та же страница доступна по нескольким адресам, и поиск не понимает, какой из них главный: вес размазывается, позиции падают. Классика: example.com, www.example.com, example.com/, example.com/?utm_source=.... Для поиска это четыре разных URL с одинаковым контентом.
Лечится это canonical-ссылкой: на каждой странице явно указываешь её единственный правильный адрес. У меня канонический домен один, apex evolkov.tech без www, и canonical на каждой странице ведёт на него. Проставляю через useHead.
<script setup lang="ts">
const route = useRoute()
const canonical = `https://evolkov.tech${route.path}`
useHead({
link: [{ rel: 'canonical', href: canonical }],
})
</script>
Пара правил, которые уберегут от боли. Canonical должен быть абсолютным, с протоколом и доменом, а не относительным. Он должен указывать сам на себя на нормальной странице, а не на другую (self-referencing canonical для уникальных страниц). Домен в canonical должен совпадать с тем, что в sitemap и во внутренних ссылках, до последней буквы: если canonical на apex, а ссылки на www, ты сам себе плодишь путаницу. И редиректы 301 с www на apex вешаешь на уровне сервера или прокси, а не только в canonical.
Sitemap и robots.txt
Две служебные вещи, без которых поиск будет искать твои страницы дольше и хуже. Карта сайта говорит боту, что вообще есть на сайте. Robots говорит, куда можно ходить, а куда нельзя.
@nuxtjs/sitemap
Модуль собирает sitemap.xml автоматически. Главное правильно задать канонический домен в конфиге, иначе в карту попадут ссылки не на тот хост.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/sitemap'],
site: {
url: 'https://evolkov.tech', // канонический домен, apex без www
},
sitemap: {
// статические роуты модуль находит сам; динамику отдаём списком
sources: ['/api/__sitemap__/urls'],
// в карту не тащим служебное и юр-страницы
exclude: ['/admin/**', '/privacy', '/cookies'],
},
})
Статические роуты (/, /uslugi, услуги) модуль подхватывает сам из файловой маршрутизации. Динамику (посты блога, карточки) отдаёшь через серверный источник, который возвращает список URL из твоего контента или базы. На выходе sitemap.xml, а при мультиязычности sitemap_index.xml с отдельными картами на локаль.
@nuxtjs/robots
Модуль генерит robots.txt и заодно умеет проставлять noindex на нужных роутах. Юридические и служебные страницы держим вне индекса, но обычно с follow, чтобы вес по ссылкам всё же перетекал.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/robots'],
robots: {
// всем ботам всё открыто, кроме служебного
disallow: ['/admin', '/api'],
// юр-страницы: не индексировать, но по ссылкам ходить можно
groups: [
{ userAgent: ['*'], allow: ['/'], disallow: ['/admin', '/api'] },
],
},
})
Самая опасная ошибка тут одна, и она обнуляет весь сайт. Disallow: / в robots.txt закрывает от поиска вообще всё. Такое случается, когда прод по недосмотру уезжает с настройками стейджинга, где сайт закрыт от индексации намеренно. Первое, что я проверяю в аудите чужого сайта, который «не индексируется», это открыть сайт/robots.txt глазами. В половине случаев проблема прямо там.
Structured data (JSON-LD)
Разметка schema.org это способ объяснить поиску, что за сущность на странице: организация, человек, статья, товар, хлебные крошки. Она не поднимает позиции напрямую, но даёт расширенные сниппеты (звёзды, крошки, автор, дата) и питает knowledge-графы Google и Яндекса. Для персонального бренда это ещё и способ связать сайт с личностью автора.
В Nuxt удобнее всего через nuxt-schema-org: он даёт типизированные хелперы, которые сами раскладываются в валидный JSON-LD и подставляются в <head>.
// Организация и автор: обычно в главном лейауте, на весь сайт
useSchemaOrg([
defineOrganization({
name: 'evolkov.tech',
url: 'https://evolkov.tech',
logo: 'https://evolkov.tech/logo.png',
}),
])
// На странице статьи: разметка Article с автором и датами
useSchemaOrg([
defineArticle({
headline: post.value?.title,
datePublished: post.value?.date,
dateModified: post.value?.updated,
author: { name: 'Евгений Волков', url: 'https://evolkov.tech/cv' },
}),
])
Что стоит размечать в первую очередь. Organization или Person на весь сайт, чтобы поиск знал, кто за ним стоит. Article на постах блога, с автором и датами. BreadcrumbList для хлебных крошек, они часто попадают прямо в сниппет выдачи. FAQPage, если на странице есть реальный блок вопросов-ответов. Одно предостережение: не размечай того, чего нет на странице, и не рисуй фейковый aggregateRating со звёздами, которых у тебя нет. Google за выдуманную разметку карает, а не награждает.
Мультиязычный SEO (i18n) и hreflang
Если сайт на нескольких языках, к техническому SEO добавляется отдельный слой: поиск должен понимать, что русская и английская версии страницы это не дубли, а языковые варианты одного контента. За это отвечает hreflang.
В Nuxt это закрывает @nuxtjs/i18n. Модуль знает про твои локали, строит для них роуты и, если включить генерацию SEO-тегов и задать baseUrl, сам проставляет alternate-ссылки с hreflang в голову каждой страницы.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n'],
i18n: {
baseUrl: 'https://evolkov.tech', // нужен для абсолютных hreflang
defaultLocale: 'ru',
strategy: 'prefix_except_default', // RU на /, EN на /en
locales: [
{ code: 'ru', language: 'ru-RU' },
{ code: 'en', language: 'en-US' },
],
},
})
Дальше @nuxtjs/sitemap подхватывает локали и собирает мультиязычный sitemap: у каждой страницы внутри появляются блоки xhtml:link со ссылками на её языковые версии. Две вещи, которые ломают hreflang чаще всего. Первая: забыть x-default для языка по умолчанию. Вторая, и она злее: проставить hreflang правильно, но оставить canonical на всех локалях указывающим на один язык. Тогда ты своими руками говоришь поиску «английская версия это дубль русской», и он выкидывает её из индекса. Canonical на каждой локали ведёт на саму себя, hreflang связывает их как альтернативы. Это разные механизмы, и работать они должны согласованно.
Скорость и Core Web Vitals
Индексация это про то, увидит ли поиск страницу. Скорость про то, как высоко он её поставит и не сбегут ли люди раньше. И Google, и Яндекс учитывают Core Web Vitals в ранжировании: LCP (как быстро появляется главный элемент), INP (отзывчивость на клики), CLS (не прыгает ли вёрстка). Nuxt тут помогает, но не делает всё за тебя.
Что даёт из коробки. SSR и prerender рисуют первый экран раньше, LCP выигрывает. Route-level code splitting грузит только код нужной страницы. @nuxt/image отдаёт картинки в WebP нужного размера, с ленивой загрузкой ниже сгиба. Шрифты подключаются локально с сабсетом. Всё это фундамент, на котором Nuxt-сайт по скорости обгоняет типовую CMS.
Теперь реальные грабли, про которые доки молчат. Первая: prerender большого сайта жрёт память. На платформе в 1000+ страниц сборка спокойно падала с out-of-memory, пока я не дал Node запас через NODE_OPTIONS=--max-old-space-size. Если билд на CI внезапно умирает без внятной ошибки, смотри в память. Вторая: тяжёлые ассеты в первом экране. 3D-герой, видео, огромные картинки топят LCP, и никакой SSR это не спасёт, тут нужна честная оптимизация ассетов. Про то, как я ужимал 3D-модели с 16 МБ до 1,1 МБ и разбирал бандл руками, я подробно написал в отдельном разборе про то, как ускорить сайт. А чем именно замерять скорость и как читать полевые данные, разложил в гайде по проверке скорости.
Один модуль вместо десяти: @nuxtjs/seo
Ставить sitemap, robots, og-image, schema-org по отдельности можно. Но есть зонтичный модуль @nuxtjs/seo, который тянет их разом и связывает общим site.config, где домен и canonical живут в одном месте. Одна строчка в modules вместо пяти конфигов.
// nuxt.config.ts: один зонтик вместо набора отдельных модулей
export default defineNuxtConfig({
modules: ['@nuxtjs/seo'],
site: {
url: 'https://evolkov.tech',
name: 'evolkov.tech',
},
})
Что внутри зонтика и зачем это по отдельности:
| Модуль | Что делает |
|---|---|
| @nuxtjs/robots | Генерит robots.txt, ставит noindex на служебные и юр-страницы |
| @nuxtjs/sitemap | Собирает sitemap.xml (и sitemap_index при i18n) из роутов и динамики |
| nuxt-og-image | Рендерит уникальные OG-картинки превью из Vue-шаблона |
| nuxt-schema-org | Типизированный JSON-LD: Organization, Person, Article, BreadcrumbList |
| nuxt-link-checker | Ловит битые внутренние ссылки на билде, чтобы не терять вес и краулинг |
| nuxt-seo-utils | Дефолты метатегов, canonical, robots-мета, мелкие SEO-хелперы |
| @nuxtjs/site-config | Единый источник домена, имени и языка для всех модулей сразу |
Я беру зонтик на контентных проектах, где нужно всё сразу и важна консистентность домена. На мелком сайте, где хватает sitemap и пары метатегов, ставлю точечно, чтобы не тащить лишнее. Оба пути валидны, вопрос масштаба.
Как проверить, что Google реально индексирует
Вот раздел, ради которого стоит дочитать. Настроить метатеги это половина дела. Вторая половина убедиться, что поиск реально всё это видит и ест. Проверяют не на глаз, а в инструментах, и вот чеклист, по которому я прохожу каждый проект.
1. «Проверка URL» в Google Search Console. Главный инструмент. Вставляешь адрес страницы, GSC показывает, знает ли о ней Google, проиндексирована ли она, и, что важнее всего, даёт кнопку «Просмотреть отсканированную страницу». Там ты видишь HTML глазами Google, а не своими. Если контента в этом HTML нет, значит рендер сломан, и никакие метатеги не помогут.
2. Отрендеренный HTML. Не доверяй тому, что видишь в браузере. Браузер выполняет JS, а бот на первой волне нет. Смотри именно rendered HTML в GSC или через curl без JavaScript: curl -s https://твой-сайт/страница | grep '<h1'. Если h1 и текст на месте в сыром ответе, SSR работает.
3. Статус sitemap. В GSC раздел «Файлы Sitemap»: отправлена ли карта, сколько URL прочитано, есть ли ошибки. То же в Яндекс.Вебмастере для рунета. Карту нужно отправить руками один раз, сама она подхватится нескоро.
4. Ловушка «в браузере есть, у Google нет». Самая частая жалоба: «страница же открывается, почему её нет в поиске?». Открывается она потому, что твой браузер выполнил JS. Бот на первой волне мог этого не сделать. Проверка одна: rendered HTML в GSC. Если там пусто, а в браузере полно, диагноз SPA без нормального SSR.
5. Перепутанные метатеги на динамических роутах. Реальный баг, который я ловил и который тихо портит выдачу. На страницах вроде /blog/[slug] title и description задаются из данных поста. Если написать useSeoMeta({ title: post.value.title }) голым значением, а не геттером, теги проставятся на первом рендере и на всех соседних постах останется title первого открытого. В выдаче это выглядит как десяток страниц с одинаковым заголовком. Лечится геттерами, как в примере выше. Проверяется просто: открой три разных поста и глянь <title> в исходнике каждого.
+Плюсы
- SSR/prerender для SEO: контент в исходном HTML, rendered HTML в GSC не пустой, метатеги индексируются, страницы заходят в индекс с первого обхода.
- Один canonical, чистый sitemap, отправленный в GSC и Вебмастер, дают предсказуемую индексацию.
−Минусы
- SPA без SSR: rendered HTML в GSC пустой, страницы висят «обнаружены, не проиндексированы».
- Голые значения вместо геттеров в useSeoMeta плодят одинаковые title на динамических роутах.
- Забытый Disallow: / или noindex со стейджинга обнуляют индексацию всего сайта разом.
Как всё это выглядит на масштабе, видно на моём кейсе международной платформы: 1000+ страниц на Nuxt, два языка, вся эта техническая обвязка (sitemap, canonical, hreflang, structured data, prerender) собрана под капотом, и платформа стабильно растёт в поиске, а не висит вне индекса. Это и есть техническое SEO в действии, а не в теории.
Коротко и что дальше
SEO для Nuxt держится на нескольких опорах, и ни одну нельзя пропустить. Правильный режим рендера (SSR, prerender или гибрид через routeRules), чтобы бот видел готовый HTML. Метатеги через useSeoMeta с геттерами на динамике. Единый canonical и чистый sitemap с robots. Structured data через nuxt-schema-org. Hreflang на мультиязычных сайтах. И обязательная проверка в Google Search Console, что всё это реально доехало до поиска. Пропустишь рендер, и остальное бессмысленно. Пропустишь проверку, и не узнаешь, что что-то сломано.
Если сайт только выбираешь под задачу, SEO-фундамент дешевле заложить сразу, чем прикручивать потом. Про сам стек и почему Nuxt удобен под контентные проекты я разбирал в материале про разработку на Nuxt, а про то, чем SSR-сайт отличается от SPA по своей природе, в объяснении, что такое веб-приложение. А если делаешь сайт под ключ и хочешь, чтобы он индексировался с первого дня, это часть моей работы над созданием сайтов.
Я делаю это как fullstack-разработчик на каждом проекте: настраиваю индексацию с первого дня, а на готовых сайтах прохожу аудитом и чиню то, что мешает поиску. Разбор входит в мою услугу технического SEO, она стартует от 10 000 ₽. Начинаем с честного аудита: я захожу в Google Search Console и Яндекс.Вебмастер, смотрю отрендеренный HTML, sitemap, canonical и метатеги на твоём сайте, показываю, что закрывает страницы от индексации, и дальше чиним по приоритету. Если сайт на Nuxt не виден в поиске и непонятно почему, напиши мне, и разберёмся предметно. Больше живых разборов по скорости, стеку и SEO лежит в блоге.


