
Nuxt provides powerful SEO tools right out of the box. SSR, automatic pre-rendering, and convenient composables make the framework an excellent choice for projects where search engine visibility is crucial.
In this guide, we'll cover all aspects of technical SEO in Nuxt: from basic meta tags to generating dynamic OG images.
Traditional SPAs on Vue have a problem: search crawlers see a blank page before the JavaScript executes. Nuxt solves this in several ways:
| Mode | Description | When to Use |
|---|---|---|
| SSR (Universal) | Server renders HTML for each request | E-commerce, dynamic content |
| SSG (Static) | HTML is generated at build time | Blogs, documentation, landing pages |
| Hybrid | A combination of SSR and SSG for different routes | Large projects with varied content |
Important: For SEO-critical projects, avoid
ssr: falsemode. Search engines better index server-rendered content.
The @nuxtjs/seo module bundles six SEO tools into one package:
npx nuxi module add @nuxtjs/seo
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/seo'],
site: {
url: 'https://example.com',
name: 'My Site',
description: 'Site description for search engines',
defaultLocale: 'en'
}
})
The site parameter is used by all modules automatically—canonical URLs, sitemaps, and OG tags will get the correct values without code duplication.
Nuxt offers two main composables for managing meta tags. Let's look at the differences.
<script setup lang="ts">
useHead({
title: 'Page Title',
meta: [
{ name: 'description', content: 'Page description' },
{ property: 'og:title', content: 'OG Title' }
],
link: [
{ rel: 'canonical', href: 'https://example.com/page' }
],
htmlAttrs: { lang: 'en' }
})
</script>
useHead is suitable for any tags in the <head>: scripts, styles, link tags, and html/body attributes.
<script setup lang="ts">
useSeoMeta({
title: 'Page Title',
description: 'Description for search engines',
ogTitle: 'Title for social media',
ogDescription: 'Description for social media',
ogImage: 'https://example.com/og-image.png',
twitterCard: 'summary_large_image'
})
</script>
Recommendation: Use
useSeoMetafor SEO tags—it has 100+ typed parameters and protection against XSS attacks.
For dynamic content, pass getter functions:
<script setup lang="ts">
const { data: article } = await useFetch('/api/article/1')
useSeoMeta({
title: () => article.value?.title,
description: () => article.value?.excerpt,
ogImage: () => article.value?.image
})
</script>
If meta tags don't need to be reactive, wrap them in a server-only condition:
<script setup lang="ts">
if (import.meta.server) {
useSeoMeta({
robots: 'index, follow',
description: 'Static description',
ogImage: 'https://example.com/image.png'
})
}
// Only dynamic tags remain reactive
const title = ref('Dynamic Title')
useSeoMeta({
title: () => title.value
})
</script>
Open Graph determines how a link will look when shared on social media.
useSeoMeta({
ogType: 'website',
ogTitle: 'Page Title',
ogDescription: 'Short description up to 200 characters',
ogImage: 'https://example.com/og.png',
ogUrl: 'https://example.com/page',
ogLocale: 'en_US',
// Twitter/X
twitterCard: 'summary_large_image',
twitterTitle: 'Title for Twitter',
twitterDescription: 'Description for Twitter',
twitterImage: 'https://example.com/twitter.png'
})
| Parameter | Recommendation |
|---|---|
| Size | 1200×630 px |
| Format | PNG or JPG |
| File Size | Up to 8 MB |
| Aspect Ratio | 1.91:1 |
The nuxt-og-image module generates images automatically based on Vue components.
<script setup lang="ts">
defineOgImageComponent('NuxtSeo', {
title: 'Article Title',
description: 'Article description',
theme: '#00dc82',
colorMode: 'dark'
})
</script>
Create a component in components/OgImage/:
<!-- components/OgImage/BlogPost.vue -->
<template>
<div class="w-full h-full flex flex-col justify-center items-center bg-gradient-to-br from-green-400 to-blue-500 p-16">
<h1 class="text-6xl font-bold text-white text-center">
{{ title }}
</h1>
<p class="text-2xl text-white/80 mt-4">
{{ author }}
</p>
</div>
</template>
<script setup lang="ts">
defineProps<{
title: string
author: string
}>()
</script>
Usage on a page:
<script setup lang="ts">
defineOgImageComponent('BlogPost', {
title: 'How to Optimize SEO in Nuxt',
author: 'John Doe'
})
</script>
In development mode, add /__og_image__ to the URL for a live preview with hot-reload.
Structured data helps Google show rich snippets in search results.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['nuxt-schema-org'],
site: {
url: 'https://example.com',
name: 'Site Name'
}
})
<script setup lang="ts">
// Homepage
useSchemaOrg([
defineWebSite({
name: 'My Blog',
description: 'A blog about web development'
}),
defineOrganization({
name: 'My Company',
logo: '/logo.png'
})
])
</script>
<script setup lang="ts">
// Article page
useSchemaOrg([
defineArticle({
headline: 'Article Title',
description: 'Short description',
image: '/article-image.jpg',
datePublished: '2025-01-15',
dateModified: '2025-01-20',
author: {
name: 'Author Name',
url: 'https://example.com/author'
}
})
])
</script>
<script setup lang="ts">
useSchemaOrg([
defineWebPage({ '@type': 'FAQPage' }),
defineQuestion({
name: 'How to install Nuxt?',
acceptedAnswer: 'Run the command npx nuxi init my-app'
}),
defineQuestion({
name: 'What is the current version of Nuxt?',
acceptedAnswer: 'The current version is Nuxt 4.x'
})
])
</script>
Tip: Validate your markup using the Google Rich Results Test.
The @nuxtjs/sitemap module automatically collects static routes:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/sitemap'],
site: {
url: 'https://example.com'
}
})
For pages with dynamic parameters, create an API endpoint:
// server/api/__sitemap__/sitemap.ts
export default defineSitemapEventHandler(async () => {
const posts = await $fetch('/api/posts')
return posts.map(post => ({
loc: `/blog/${post.slug}`,
lastmod: post.updatedAt,
changefreq: 'weekly',
priority: 0.8
}))
})
// nuxt.config.ts
export default defineNuxtConfig({
sitemap: {
sources: ['/api/__sitemap__/urls']
}
})
// nuxt.config.ts
export default defineNuxtConfig({
robots: {
groups: [
{
userAgent: '*',
allow: '/',
disallow: ['/admin', '/api', '/private']
}
]
}
})
Or via a public/_robots.txt file:
User-agent: *
Allow: /
Disallow: /admin
Disallow: /api
Sitemap: https://example.com/sitemap.xml
Important: The module automatically blocks indexing for dev/staging environments.
The @nuxt/image module is critically important for Core Web Vitals.
npx nuxi module add @nuxt/image
<template>
<!-- Basic usage -->
<NuxtImg
src="/images/hero.jpg"
alt="Hero image"
width="1200"
height="630"
loading="lazy"
format="webp"
/>
<!-- Responsive sizes -->
<NuxtImg
src="/images/product.jpg"
alt="Product"
sizes="sm:100vw md:50vw lg:400px"
:modifiers="{ quality: 80 }"
/>
<!-- Automatic format selection -->
<NuxtPicture
src="/images/banner.jpg"
alt="Banner"
width="1920"
height="600"
/>
</template>
// nuxt.config.ts
export default defineNuxtConfig({
image: {
provider: 'cloudinary', // or ipx, imgix, vercel, etc.
cloudinary: {
baseURL: 'https://res.cloudinary.com/your-cloud/image/upload/'
},
quality: 80,
format: ['webp', 'avif', 'jpeg']
}
})
The @nuxtjs/i18n module integrates with SEO modules automatically.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/i18n', '@nuxtjs/seo'],
i18n: {
locales: [
{ code: 'uk', language: 'uk-UA', name: 'Українська' },
{ code: 'en', language: 'en-US', name: 'English' }
],
defaultLocale: 'uk',
strategy: 'prefix_except_default'
}
})
Use useLocaleHead for automatic generation:
<script setup lang="ts">
const head = useLocaleHead({
addSeoAttributes: true
})
useHead(head)
</script>
This will add:
lang attribute for <html>hreflang links for all localesNuxt SEO integrates with DevTools. Available tabs:
| Tool | Purpose |
|---|---|
| Google Search Console | Indexing, errors |
| Rich Results Test | Schema.org |
| Meta Tags Debugger | OG tags |
| PageSpeed Insights | Core Web Vitals |
To test without deploying, use ngrok:
ngrok http 3000
Then check the URL via metatags.io or the Facebook Sharing Debugger.
<title> on every page (50-60 characters)lang attribute is set