11. Metadata и SEO
🔍 Metadata и SEO в Nuxt 3
Заголовок раздела «🔍 Metadata и SEO в Nuxt 3»SEO — это не только контент, но и правильные мета-теги. Nuxt 3 предоставляет мощные инструменты для управления <head> каждой страницы: useSeoMeta, useHead, Open Graph, Twitter Cards и многое другое.
Почему SEO в Nuxt проще? 🎯
Заголовок раздела «Почему SEO в Nuxt проще? 🎯»В обычном SPA (Angular, Vue без Nuxt) страницы рендерятся JavaScript — поисковые роботы не всегда их правильно индексируют. Nuxt с SSR рендерит HTML на сервере, включая все мета-теги, что делает сайт идеальным для SEO.
useSeoMeta — современный подход ✨
Заголовок раздела «useSeoMeta — современный подход ✨»useSeoMeta — наиболее предпочтительный способ. TypeScript-типизированный, предотвращает ошибки:
<script setup lang="ts">const { data: post } = await useFetch(\`/api/posts/\${route.params.slug}\`)
useSeoMeta({ // Основные теги title: () => post.value?.title || 'Блог', description: () => post.value?.description,
// Open Graph (Facebook, Telegram, VK...) ogTitle: () => post.value?.title, ogDescription: () => post.value?.description, ogImage: () => post.value?.coverImage, ogUrl: () => \`https://myapp.com/blog/\${post.value?.slug}\`, ogType: 'article', ogSiteName: 'My Blog', ogLocale: 'ru_RU',
// Twitter Cards twitterCard: 'summary_large_image', twitterTitle: () => post.value?.title, twitterDescription: () => post.value?.description, twitterImage: () => post.value?.coverImage, twitterCreator: '@myblog',
// Канонический URL (важно для SEO!) canonical: () => \`https://myapp.com/blog/\${post.value?.slug}\`,
// Robots robots: 'index, follow',})</script>useHead — полный контроль над <head> 🎛️
Заголовок раздела «useHead — полный контроль над <head> 🎛️»<script setup>useHead({ title: 'Страница о нас',
// Шаблон заголовка titleTemplate: (titleChunk) => { return titleChunk ? \`\${titleChunk} — My Site\` : 'My Site' },
// Мета-теги meta: [ { name: 'description', content: 'Описание страницы' }, { name: 'keywords', content: 'nuxt, vue, seo' }, { name: 'author', content: 'John Doe' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { name: 'theme-color', content: '#00dc82' },
// Open Graph { property: 'og:title', content: 'Заголовок для соцсетей' }, { property: 'og:type', content: 'website' },
// Верификация поисковиков { name: 'google-site-verification', content: 'token123' }, { name: 'yandex-verification', content: 'yandex-token' }, ],
// Ссылки link: [ { rel: 'canonical', href: 'https://myapp.com/about' }, { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }, { rel: 'apple-touch-icon', href: '/apple-touch-icon.png' }, { rel: 'alternate', hreflang: 'en', href: 'https://myapp.com/en/about' }, { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, ],
// Скрипты script: [ { // JSON-LD структурированные данные для Google type: 'application/ld+json', innerHTML: JSON.stringify({ '@context': 'https://schema.org', '@type': 'WebSite', name: 'My Site', url: 'https://myapp.com', }) } ],
// HTML атрибуты htmlAttrs: { lang: 'ru', dir: 'ltr', },
// Body атрибуты bodyAttrs: { class: 'dark-theme' },})</script>Глобальные мета-теги в nuxt.config.ts 🌍
Заголовок раздела «Глобальные мета-теги в nuxt.config.ts 🌍»export default defineNuxtConfig({ app: { head: { // Шаблон для всех страниц titleTemplate: '%s | My Awesome Site', title: 'My Awesome Site', // Дефолтный заголовок
htmlAttrs: { lang: 'ru', },
meta: [ { charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { name: 'format-detection', content: 'telephone=no' }, // Open Graph дефолты { property: 'og:site_name', content: 'My Awesome Site' }, { property: 'og:type', content: 'website' }, { property: 'og:locale', content: 'ru_RU' }, // Twitter { name: 'twitter:card', content: 'summary' }, { name: 'twitter:site', content: '@mysite' }, ],
link: [ { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }, { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, ], } }})definePageMeta для SEO 📋
Заголовок раздела «definePageMeta для SEO 📋»<script setup>definePageMeta({ // Отключить для страниц без SEO ценности robots: false, // Не индексировать
// Для admin страниц layout: 'admin',})
// Динамически устанавливаем SEO после загрузкиconst { data } = await useFetch('/api/product/1')
useSeoMeta({ title: data.value?.name, description: data.value?.description, ogImage: data.value?.imageUrl,})</script>Robots.txt через Server Route 🤖
Заголовок раздела «Robots.txt через Server Route 🤖»export default defineEventHandler((event) => { const config = useRuntimeConfig() const isProduction = process.env.NODE_ENV === 'production'
const robots = isProduction ? \`User-agent: *Allow: /
Disallow: /admin/Disallow: /api/Disallow: /_nuxt/
# Карта сайтаSitemap: \${config.public.siteUrl}/sitemap.xml\` : \`User-agent: *Disallow: /\` // В разработке — запрещаем всё
setHeader(event, 'Content-Type', 'text/plain; charset=utf-8') setHeader(event, 'Cache-Control', 'public, max-age=86400')
return robots})Sitemap с @nuxtjs/sitemap 🗺️
Заголовок раздела «Sitemap с @nuxtjs/sitemap 🗺️»npm install @nuxtjs/sitemapexport default defineNuxtConfig({ modules: ['@nuxtjs/sitemap'],
site: { url: 'https://myapp.com', name: 'My Awesome Site', },
sitemap: { // Статические URL urls: [ { loc: '/', priority: 1.0, changefreq: 'daily' }, { loc: '/about', priority: 0.8 }, { loc: '/contact', priority: 0.5 }, ],
// Динамические URL из API sources: [ '/api/__sitemap__/urls', // Твой API endpoint ],
// Настройки exclude: ['/admin/**', '/private/**'], defaults: { changefreq: 'weekly', priority: 0.5, }, }})export default defineSitemapEventHandler(async (event) => { const posts = await db.posts.findMany({ where: { published: true } })
return posts.map(post => ({ loc: \`/blog/\${post.slug}\`, lastmod: post.updatedAt, priority: 0.8, changefreq: 'monthly', }))})JSON-LD структурированные данные 📊
Заголовок раздела «JSON-LD структурированные данные 📊»<script setup>const { data: post } = await useFetch(\`/api/posts/\${route.params.slug}\`)
// Структурированные данные для Google Rich ResultsuseHead({ script: [ { type: 'application/ld+json', innerHTML: computed(() => JSON.stringify({ '@context': 'https://schema.org', '@type': 'BlogPosting', headline: post.value?.title, description: post.value?.description, image: post.value?.coverImage, datePublished: post.value?.publishedAt, dateModified: post.value?.updatedAt, author: { '@type': 'Person', name: post.value?.author?.name, }, publisher: { '@type': 'Organization', name: 'My Blog', logo: { '@type': 'ImageObject', url: 'https://myapp.com/logo.png' } }, })) } ]})</script>Composable для SEO 🪝
Заголовок раздела «Composable для SEO 🪝»export const useSeo = (options: { title: string description: string image?: string type?: string noindex?: boolean}) => { const config = useRuntimeConfig() const route = useRoute() const siteUrl = config.public.siteUrl as string const fullUrl = \`\${siteUrl}\${route.path}\`
useSeoMeta({ title: options.title, description: options.description,
ogTitle: options.title, ogDescription: options.description, ogImage: options.image || \`\${siteUrl}/og-default.png\`, ogUrl: fullUrl, ogType: options.type || 'website',
twitterCard: options.image ? 'summary_large_image' : 'summary', twitterTitle: options.title, twitterDescription: options.description, twitterImage: options.image,
canonical: fullUrl, robots: options.noindex ? 'noindex, nofollow' : 'index, follow', })}<script setup>useSeo({ title: 'О компании — My Site', description: 'Узнайте больше о нашей компании и команде', image: '/images/about-og.jpg',})</script>