Nuxt SEO Optimization: A Complete Guide from Basic Setup to Advanced Techniques

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.
Why Nuxt is Ideal for SEO
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.
Quick Start with @nuxtjs/seo
The @nuxtjs/seo module bundles six SEO tools into one package:
- Sitemap — generates an XML sitemap
- Robots — manages robots.txt and meta tags
- OG Image — dynamic images for social media
- Schema.org — structured JSON-LD data
- Link Checker — checks for broken links
- SEO Utils — additional utilities
Installation
npx nuxi module add @nuxtjs/seo
Basic Configuration
// 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.
Meta Tags: useHead vs useSeoMeta
Nuxt offers two main composables for managing meta tags. Let's look at the differences.
useHead — The Universal Tool
<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.
useSeoMeta — Typed and Secure
<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.
Reactive Meta Tags
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>
Performance Optimization
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 and Social Media
Open Graph determines how a link will look when shared on social media.
Minimum Tag Set
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'
})
OG Image Requirements
| Parameter | Recommendation |
|---|---|
| Size | 1200×630 px |
| Format | PNG or JPG |
| File Size | Up to 8 MB |
| Aspect Ratio | 1.91:1 |
Dynamic OG Images
The nuxt-og-image module generates images automatically based on Vue components.
Using Built-in Templates
<script setup lang="ts">
defineOgImageComponent('NuxtSeo', {
title: 'Article Title',
description: 'Article description',
theme: '#00dc82',
colorMode: 'dark'
})
</script>
Custom Template
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>
Preview
In development mode, add /__og_image__ to the URL for a live preview with hot-reload.
Schema.org and Structured Data
Structured data helps Google show rich snippets in search results.
Basic Setup
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['nuxt-schema-org'],
site: {
url: 'https://example.com',
name: 'Site Name'
}
})
Page Types
<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>
FAQ Schema
<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.
Sitemap and robots.txt
Automatic Sitemap Generation
The @nuxtjs/sitemap module automatically collects static routes:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/sitemap'],
site: {
url: 'https://example.com'
}
})
Dynamic Routes
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']
}
})
Configuring robots.txt
// 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.
Image Optimization
The @nuxt/image module is critically important for Core Web Vitals.
Installation
npx nuxi module add @nuxt/image
Usage
<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>
Image Providers
// 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']
}
})
i18n and Multilingual SEO
The @nuxtjs/i18n module integrates with SEO modules automatically.
Configuration
// 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'
}
})
hreflang tags
Use useLocaleHead for automatic generation:
<script setup lang="ts">
const head = useLocaleHead({
addSeoAttributes: true
})
useHead(head)
</script>
This will add:
- The
langattribute for<html> hreflanglinks for all locales- A canonical URL for the current locale
Testing and Debugging
Nuxt DevTools
Nuxt SEO integrates with DevTools. Available tabs:
- SEO — view all meta tags
- OG Image — preview OG images
- Schema.org — validate structured data
- Sitemap — view generated URLs
- Robots — view current rules
External Tools
| Tool | Purpose |
|---|---|
| Google Search Console | Indexing, errors |
| Rich Results Test | Schema.org |
| Meta Tags Debugger | OG tags |
| PageSpeed Insights | Core Web Vitals |
Local OG Tag Testing
To test without deploying, use ngrok:
ngrok http 3000
Then check the URL via metatags.io or the Facebook Sharing Debugger.
Pre-Release Checklist
Basic SEO
- Unique
<title>on every page (50-60 characters) - Meta description on every page (150-160 characters)
- Canonical URL is configured
- Robots meta tag is correct
- HTML
langattribute is set
Technical SEO
- sitemap.xml is generated and accessible
- robots.txt is configured
- 404 page returns the correct status code
- Redirects from HTTP to HTTPS
- Trailing slashes are consistent
Social Media
- OG tags on all pages
- OG image is 1200×630 px
- Twitter Card tags
- Verified in Facebook Debugger
Structured Data
- Organization/Person schema on the homepage
- Article schema on articles
- BreadcrumbList for navigation
- Validated via Rich Results Test
Performance
- Lighthouse Performance > 90
- LCP < 2.5s
- CLS < 0.1
- Images are optimized (WebP/AVIF)