Nuxt SEO Configuration
Nuxt 3 centralizes rendering decisions in a single routeRules map and injects metadata through composables that run during server-side setup β so the same config file that decides whether a route is static, incremental, or server-rendered also governs what a crawler sees. Getting routeRules, useSeoMeta, and server-side canonical wiring right is what keeps CMS-driven pages fully indexable across a mixed-volatility site.
Prerequisites
Confirm the following before configuring Nuxt for SEO:
- Nuxt 3.x running on Nitro β
routeRulesand theuseSeoMeta/useHeadcomposables are built in - A CMS module or client exposing content over REST or GraphQL for
useFetch/useAsyncData NUXT_PUBLIC_SITE_URLset to a protocol-prefixed absolute domain in every build environment β resolved viauseRuntimeConfig().public.siteUrl, never from the request hostname- A deploy target for Nitro (Node server, or an edge preset such as Cloudflare) that honors per-route cache headers
curlavailable in CI to read response headers and confirm each routeβs rendering mode
If your content spans multiple languages, pair this setup with hreflang and international routing in headless so locale alternates are emitted correctly per route.
How a Request Resolves Through routeRules to the Crawler
The diagram shows a request matching a routeRules pattern, picking up its rendering mode, gaining metadata from useSeoMeta, and being served by Nitro.
Step-by-Step Implementation Workflow
Step 1 β Define routeRules per URL pattern
Map each section of the site to a rendering mode in nuxt.config. Glob patterns let volatile and stable content coexist:
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/': { prerender: true },
'/blog/**': { isr: 3600 }, // regenerate at most hourly
'/products/**': { swr: 600 }, // stale-while-revalidate 10 min
'/account/**': { ssr: false }, // client-only, keep out of the index
},
});
Match the mode to how often the content changes and whether crawlers must read it. Anything indexable must stay server-rendered (prerender, isr, swr, or default ssr), never ssr: false.
Step 2 β Set metadata with useSeoMeta
Inside the pageβs setup, call useSeoMeta with values pulled from the CMS:
<!-- pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute();
const { data: entry } = await useFetch(`/api/cms/${route.params.slug}`);
useSeoMeta({
title: () => entry.value?.title,
description: () => entry.value?.description,
ogTitle: () => entry.value?.title,
ogType: 'article',
});
</script>
Because setup runs on the server for SSR and prerendered routes, these tags are serialized into the initial HTML.
Step 3 β Emit canonical and hreflang server-side
Add a useHead link array that resolves absolute URLs from runtime config:
<script setup lang="ts">
const route = useRoute();
const { public: { siteUrl } } = useRuntimeConfig();
const canonical = `${siteUrl}${route.path}`;
useHead({
link: [
{ rel: 'canonical', href: canonical },
{ rel: 'alternate', hreflang: 'en', href: `${siteUrl}/en${route.path}` },
{ rel: 'alternate', hreflang: 'de', href: `${siteUrl}/de${route.path}` },
{ rel: 'alternate', hreflang: 'x-default', href: canonical },
],
});
</script>
Building from siteUrl β not the request hostname β is what keeps staging domains out of production tags, the same principle covered in canonical URL enforcement.
Step 4 β Wire the sitemap and robots routes
Expose the sitemap as a Nitro server route driven by CMS data:
// server/routes/sitemap.xml.ts
export default defineEventHandler(async (event) => {
const { public: { siteUrl } } = useRuntimeConfig();
const slugs = await $fetch<string[]>('/api/cms/slugs');
const urls = slugs
.map((s) => `<url><loc>${siteUrl}/blog/${s}</loc></url>`)
.join('');
setHeader(event, 'Content-Type', 'application/xml');
return `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls}</urlset>`;
});
Step 5 β Validate the applied rendering mode
curl each route and inspect cache and age headers to confirm routeRules applied the mode you intended. See the Validation Protocol section below.
Framework-Specific Code Examples
Nuxt 3
Per-route cache-control and redirects also live in routeRules, so headers stay declarative alongside the rendering mode:
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/blog/**': {
isr: 3600,
headers: { 'cache-control': 's-maxage=3600, stale-while-revalidate=60' },
},
'/old-path': { redirect: { to: '/new-path', statusCode: 301 } },
},
});
SEO impact: The isr window keeps blog HTML fresh without a full rebuild, the s-maxage header lets the CDN serve cached HTML to crawlers, and the 301 rule consolidates link equity β all without per-request server logic that could slow crawler TTFB.
Validation: curl -sI https://example.com/blog/my-post | grep -i 'cache-control\|age' β the header must reflect the routeRules value.
Next.js App Router
The nearest Next.js equivalent is route segment config: export const revalidate mirrors isr, and redirects() in next.config mirrors the redirect rule. The full pattern is documented in Next.js App Router SEO Configuration.
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // ISR window, equivalent to Nuxt isr: 3600
SEO impact: Matches Nuxtβs incremental regeneration so crawlers receive cached static HTML that refreshes on a fixed window.
Validation: Confirm the route shows an ISR marker in the Next.js build output.
SvelteKit
SvelteKit expresses rendering mode through page options rather than a central map β export const prerender and ssr on each route. See SvelteKit SEO Configuration for the equivalent wiring.
// src/routes/blog/[slug]/+page.ts
export const prerender = true; // equivalent to Nuxt prerender: true
SEO impact: Produces static HTML at build time, matching a Nuxt prerender rule.
Validation: Confirm the route is listed under prerendered pages in the SvelteKit build log.
HTTP Headers & CDN Directives Reference
| Header | Required value | Rationale |
|---|---|---|
Cache-Control |
s-maxage=3600, stale-while-revalidate=60 |
Lets the CDN serve cached HTML to crawlers while isr/swr refreshes in the background |
Content-Type |
text/html; charset=utf-8 |
Ensures rendered routes are parsed as HTML |
Link |
<https://example.com/blog/my-post>; rel="canonical" |
Optional HTTP-layer canonical for edge-served routes |
X-Robots-Tag |
index, follow |
Confirms Nitro is not blocking indexation of production routes |
Content-Type (sitemap) |
application/xml |
Required so the Nitro sitemap route is parsed by crawlers |
Validation Protocol
Confirm the rendering mode per route
# ISR/SWR routes expose CDN cache + age headers; pure SSR does not
curl -sI https://example.com/blog/my-post | grep -i 'cache-control\|age\|x-nitro'
An age header that increments on repeated requests confirms the route is being served from the incremental cache rather than rendered fresh each time.
Confirm canonical and hreflang in the raw HTML
curl -s https://example.com/blog/my-post | grep -i 'canonical\|hreflang'
Every declared locale plus x-default must appear, and the canonical must be absolute and match the served path.
Assert SEO tags in Lighthouse CI
// lighthouserc.js
module.exports = {
assert: {
assertions: {
'canonical': ['error', { minScore: 1 }],
'meta-description': ['error', { minScore: 1 }],
'hreflang': ['error', { minScore: 1 }],
},
},
};
Troubleshooting
| Symptom | Root cause | Fix |
|---|---|---|
| Metadata missing in view-source | Route matched an ssr: false rule |
Change the routeRules entry to a server-rendered mode for indexable content |
Canonical shows localhost or staging domain |
Built from request hostname instead of runtime config | Resolve from useRuntimeConfig().public.siteUrl in useHead |
| ISR content never updates | No revalidation window or webhook to purge | Set isr: <seconds> and purge on CMS publish |
| hreflang tags omitted on some locales | useHead link array missing entries |
Generate one alternate per locale plus x-default from the locale list |
| Crawl budget wasted on faceted params | No rule excluding parameterized URLs | Add canonical/noindex handling β see crawl budget impact in headless |
Pages in This Section
- Configuring routeRules for Hybrid Rendering SEO in Nuxt β a focused walkthrough of mapping URL patterns to
prerender,isr,swr, andssrwith per-route headers and redirects
Frequently Asked Questions
What rendering modes can routeRules set?
routeRules can set prerender for build-time static HTML, isr for incremental static regeneration with a revalidation window, swr for stale-while-revalidate caching, and ssr: false for a client-only shell. Each rule is keyed by a URL glob so different sections of the site can use different modes. Keep anything indexable on a server-rendered mode.
How do I set canonical and hreflang server-side in Nuxt?
Call useHead in the page setup with a link array containing a rel="canonical" entry and one rel="alternate" hreflang entry per locale, building absolute URLs from useRuntimeConfig().public.siteUrl. Because setup runs on the server for SSR and prerendered routes, the tags land in the initial HTML. For full locale coverage, follow hreflang and international routing in headless.
Does useSeoMeta render tags in the initial HTML?
Yes, for server-rendered and prerendered routes. useSeoMeta runs during setup, which executes on the server, so the title, description, and Open Graph tags are serialized into the first HTML response. On routes forced to client-only rendering the tags are added after hydration instead, which crawlers may not see.
Part of: Framework-Specific SEO Configuration for Headless Stacks
Related
- Next.js App Router SEO Configuration β the segment-config equivalent of
routeRulesin Next.js - SvelteKit SEO Configuration β page-option rendering control in SvelteKit
- hreflang & International Routing in Headless β emit reciprocal locale alternates alongside canonicals
- Canonical URL Enforcement β resolve absolute canonicals from runtime config, not the request host
- Crawl Budget Impact in Headless β keep parameterized routes from wasting crawl quota