При разработке на Vue.js рано или поздно встаёт вопрос: как сделать так, чтобы страницы индексировались поисковиками, загружались мгновенно и при этом не превращались в монструозный SPA с тонной JavaScript на клиенте? Ответ - Nuxt.js. Это open-source фреймворк, построенный поверх Vue.js, который решает проблему серверного рендеринга, маршрутизации и SEO "из коробки". Если вы уже знакомы с основами Vue.js, разобраться с Nuxt будет несложно - он построен на тех же принципах, но добавляет мощные абстракции поверх.
Зачем нужен серверный рендеринг
Обычное Vue-приложение работает по принципу CSR (Client-Side Rendering): браузер получает практически пустой HTML-файл, затем скачивает и выполняет JavaScript-бандл, и только после этого Vue рендерит интерфейс. Пользователь в это время смотрит на белый экран.
Проблемы CSR:
- Поисковые роботы могут не дождаться отрисовки и не проиндексировать контент
- Первая загрузка страницы медленная - особенно на мобильных устройствах и плохом интернете
- Социальные сети не видят Open Graph-разметку - ссылки на ваш сайт в мессенджерах и соцсетях выглядят уныло, без превью
SSR (Server-Side Rendering) выглядит иначе: Vue-компоненты рендерятся в HTML на сервере, браузер получает полностью готовую страницу, и пользователь видит контент сразу. После загрузки JavaScript происходит гидратация - Vue "оживляет" статическую разметку, навешивая обработчики событий и делая страницу интерактивной. О том, как Vue управляет DOM-деревом через механизм Virtual DOM, мы рассказывали в отдельной статье.
Что даёт SSR:
- SEO: краулеры Google, Яндекс и Bing получают готовый HTML и индексируют его без проблем
- Скорость: показатель First Contentful Paint (FCP) минимален - контент виден сразу
- Доступность: страница читается скринридерами до загрузки JS
- Социальные превью: Open Graph и Twitter Cards работают корректно
Минусы тоже есть: нужен сервер для рендеринга, нельзя использовать browser-only API в произвольном месте кода, и сама разработка чуть сложнее, чем в чистом SPA. Но Nuxt.js берёт большую часть этой боли на себя.
Nuxt также поддерживает статическую генерацию (SSG) и гибридный рендеринг - но к этому мы вернёмся позже.

Установка и структура проекта
Создать проект проще простого:
npm create nuxt@latest my-appПосле пары вопросов (нужен ли TypeScript, ESLint, Pinia и т.д.) получаем готовую структуру:
my-app/
app/
pages/ # файловая маршрутизация
components/ # автоимпортируемые компоненты
composables/ # переиспользуемая логика
layouts/ # шаблоны страниц
middleware/ # защита роутов
assets/ # стили, изображения
server/
api/ # серверные эндпоинты
nuxt.config.ts # конфигурацияТочка входа - app/app.vue. В отличие от обычного Vue-проекта, здесь нет main.js: Nuxt сам создаёт и конфигурирует приложение. Минимальный app.vue выглядит так:
<template>
<div>
<NuxtPage />
</div>
</template><NuxtPage /> - это место, куда рендерится текущая страница. Всё, можно запускать npm run dev и открывать http://localhost:3000.
Файловая маршрутизация
Одна из главных фишек Nuxt - роутинг на основе файловой структуры. Не нужно вручную настраивать Vue Router: создаёте файл в app/pages/, и он автоматически становится маршрутом.
app/pages/
index.vue → /
about.vue → /about
blog/
[slug].vue → /blog/:slugДинамические параметры - в квадратных скобках. Значение параметра можно получить через useRoute():
<script setup lang="ts">
const route = useRoute()
console.log(route.params.slug) // для /blog/my-post → 'my-post'
</script>Для навигации используется <NuxtLink> - он работает как <RouterLink>, но с автоматическим префетчингом: когда ссылка попадает во viewport, Nuxt заранее подгружает компонент страницы и данные. Переход происходит мгновенно.
<template>
<NuxtLink to="/about">О проекте</NuxtLink>
</template>Для защиты страниц есть route middleware. Например, закрываем админку от неавторизованных:
// app/middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
if (!isAuthenticated()) {
return navigateTo('/login')
}
})<script setup lang="ts">
definePageMeta({ middleware: 'auth' })
</script>Получение данных: useFetch и useAsyncData
В обычном Vue вы бы написали fetch внутри onMounted. В Nuxt это создаст проблему: данные не попадут в серверный рендеринг, поисковик увидит пустую страницу, а на клиенте данные загрузятся повторно.
Nuxt решает это композаблами useFetch и useAsyncData. Они выполняют запрос на сервере, сохраняют результат в payload и передают его на клиент - без повторной загрузки.
<script setup lang="ts">
const { data: posts, status } = await useFetch('/api/posts')
</script>
<template>
<div v-if="status === 'pending'">Загрузка...</div>
<div v-else>
<article v-for="post in posts" :key="post.id">
<h2>{{ post.title }}</h2>
</article>
</div>
</template>useFetch подходит для большинства случаев. Если API нестандартное или запросов несколько - используйте useAsyncData:
<script setup lang="ts">
const { data } = await useAsyncData('blog-data', async () => {
const [posts, categories] = await Promise.all([
$fetch('/api/posts'),
$fetch('/api/categories'),
])
return { posts, categories }
})
</script>Полезные опции:
lazy: true- не блокирует навигацию, пока данные грузятся (страница откроется сразу, данные подтянутся позже)server: false- запрос только на клиенте (для не-SEO-чувствительных данных)transform- обработка данных перед сохранением в payloadwatch- перезапрос при изменении реактивных зависимостей
SEO и мета-теги
SSR даёт свободный доступ к управлению <head> - и Nuxt делает это максимально удобно через композабл useHead или его типобезопасную версию useSeoMeta:
<script setup lang="ts">
useSeoMeta({
title: 'Nuxt.js: серверный рендеринг для Vue',
description: 'Разбираем Nuxt.js: SSR, маршрутизация и SEO-оптимизация',
ogTitle: 'Nuxt.js: серверный рендеринг для Vue',
ogDescription: 'Как превратить Vue-приложение в SEO-дружественный сайт',
ogImage: 'https://mysite.com/og-image.png',
twitterCard: 'summary_large_image',
})
</script>Для страниц блога мета-теги должны быть динамическими - зависеть от данных поста. Это решается просто:
<script setup lang="ts">
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`)
useSeoMeta({
title: post.value?.title,
description: post.value?.excerpt,
ogImage: post.value?.coverImage,
})
</script>Глобальные настройки (favicon, язык, title-шаблон) задаются в nuxt.config.ts:
export default defineNuxtConfig({
app: {
head: {
htmlAttrs: { lang: 'ru' },
link: [{ rel: 'icon', href: '/favicon.ico' }],
},
},
})titleTemplate позволяет автоматически добавлять название сайта к заголовкам страниц - '%s — Мой Блог'.
Гибридный рендеринг: SSR + SSG в одном проекте
Уникальная возможность Nuxt - задавать стратегию рендеринга для отдельных маршрутов через routeRules:
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true }, // главная генерится на билде
'/blog/**': { swr: 3600 }, // кэш на сервере, перегенерация в фоне
'/admin/**': { ssr: false }, // клиентский рендеринг для админки
},
})Это даёт лучшее из двух миров: статические страницы для максимальной скорости и SSR для динамического контента - и всё в одном приложении.

Деплой
Nuxt через движок Nitro собирает приложение в универсальный .output/:
npm run build
node .output/server/index.mjsМожно задеплоить куда угодно:
- Node.js-сервер: стандартный пресет, PM2 для управления процессами
- Статический хостинг:
npm run generate- получите HTML-файлы для Netlify, GitHub Pages и т.д. - Edge-платформы: Vercel, Cloudflare Workers, Deno Deploy - через
NITRO_PRESET
Заключение
Nuxt.js превращает Vue из клиентской библиотеки в полноценный full-stack инструмент. SSR решает проблемы с SEO и начальной загрузкой, файловый роутинг убирает рутину, а useFetch снимает головную боль с дублированием запросов.
Nuxt стоит выбрать, если вы делаете контентный сайт, блог, интернет-магазин или любой проект, где важна индексация и скорость первой загрузки. Если же у вас внутренняя админка или SPA без требований к SEO - хватит и обычного Vue.
💬 А вы используете SSR в своих проектах? Делитесь в комментариях - какой фреймворк выбрали и почему.


Обязательные поля помечены *