Nuxt SEO

Nuxt ships server-side rendering by default, but that's a per-route setting, not a guarantee — full HTML to crawlers only on routes that keep it. Rendering modes, useSeoMeta(), the @nuxtjs/seo toolkit, hydration and Nitro caveats, Core Web Vitals, and the mistakes that quietly cost you.

First published: Jun 26, 2026 · Last updated: Jul 27, 2026 · Advanced
demand #5 in JavaScript Frameworks#25 in Platform SEO#196 in Technical SEO#262 on the site
1 evidence signal on this page

Nuxt server-renders pages by default, so a route that keeps that default gives crawlers a complete HTML document instead of the empty shell a plain Vue SPA ships — but it's a per-route setting: routeRules or a global ssr:false can flip any route to CSR, so verify the actual route rather than assuming from the framework name. The foundational decision is rendering mode per route — SSR (default), SSG via nuxt generate, or hybrid route rules — because meta tags, schema, and sitemaps all come after that. Use useSeoMeta() for meta, treat Harlan Wilton's @nuxtjs/seo bundle as an optional third-party toolkit (not Nuxt core) for robots/sitemap/OG/schema/canonical, never reach for dynamic rendering (Google deprecated it), verify hydration/payload and Nitro deployment-preset/cache behavior independently of server HTML, and remember that AI-crawler rendering contracts vary by provider — SSR/SSG puts content in raw HTML and maximizes coverage.

TL;DR — Nuxt’s default is server-side rendering, so a route that keeps that default gets a complete DOM instead of the empty shell a plain Vue SPA ships — but that’s a per-route outcome: ssr: false or a routeRules override can turn any route into CSR or hybrid, so test the actual route, not the project name. Rendering strategy is the foundational decision — SSR (default), SSG (nuxt generate), or hybrid routeRules — because meta, schema, and sitemaps all come after it. Use useSeoMeta() for meta tags (useHead() for the rest of the head, server-only variants when you don’t need reactivity), and treat Harlan Wilton’s @nuxtjs/seo bundle as an optional third-party toolkit for robots/sitemap/OG/schema/canonical — not part of Nuxt core, and not proof its output is correct until you check it. Never use dynamic rendering (Google deprecated it). AI-crawler rendering varies by provider — SSR/SSG maximizes raw-HTML coverage. Beyond the initial HTML, verify hydration/payload, Nitro deployment presets and caching, and direct HTTP status independently — server HTML alone doesn’t prove any of those. The usual JS-SEO rules still apply: real <a href> links, don’t block JS/CSS, watch rendered vs. raw HTML.

Nuxt is Vue’s answer to the SPA problem

Vue, by itself, ships a single-page application: an HTML shell plus JavaScript that builds the DOM in the browser. Nuxt is the meta-framework on top of Vue (running on the Nitro server engine in Nuxt 3, with Nuxt 4 following in 2025), and its whole reason for existing — from an SEO standpoint — is that it renders on the server by default. Every page arrives as a fully-formed HTML document, which is exactly what Googlebot wants to read without having to execute JavaScript first. Plain Vue SEO is its own topic with its own failure modes; here I’m assuming you’ve picked Nuxt precisely so you don’t have to fight the SPA problem.

This is the same point I make about JavaScript generally: “any kind of SSR, static rendering, and prerendering setup is going to be fine for search engines. Gatsby, Next, Nuxt, etc., are all great.” Nuxt’s defaults are pointed the right way. Most Nuxt SEO problems are people turning those defaults off, or layering mistakes on top of them.

Rendering strategy is the foundation — decided per route, not per project

Before meta tags, before schema, before sitemaps, the question that decides everything is how does the HTML get produced for this specific route? Nuxt documents universal (server) rendering as the app-wide default, but that default isn’t a route-level guarantee: a global ssr: false switches the whole app to client rendering, and routeRules can assign a different mode to any URL pattern. “This is a Nuxt app” tells you nothing about how one particular page renders — you have to check the route. Nuxt gives you five strategies:

ModeHow you set itSEO impactBest for
Universal / SSRDefault (ssr: true)Excellent — full HTML every requestDynamic, personalized content
Static / SSGnuxt generateExcellent — HTML built at deploy timeBlogs, docs, marketing
HybridrouteRules per routeExcellent — mix per routeLarge mixed-content sites
SPA / CSRssr: falsePoor for indexed contentDashboards, admin panels
Edge-sideDeployment targetExcellent — low TTFBGlobal performance

The official Nuxt docs are blunt about why client-side rendering is the wrong choice for content: “Indexing and updating the content delivered via client-side rendering takes more time”, whereas with server (universal) rendering “web crawlers can directly index the page’s content.” Evidence for this claim Nuxt documents that universal rendering delivers HTML content immediately and allows crawlers to index it directly. Scope: Nuxt rendering; no indexing guarantee. Confidence: high · Verified: Nuxt: Rendering modes (Nuxt rendering docs.) CSR (ssr: false) is positioned for back-office, dashboards, and games — not anything you want indexed.

Hybrid rendering is the power move for large sites. Route rules in nuxt.config.ts let you set the rendering and caching mode per URL pattern:

routeRules: {
  '/blog/**':     { prerender: true },        // SSG for the blog
  '/product/**':  { swr: 3600 },              // ISR-style: regenerate hourly
  '/admin/**':    { ssr: false },             // SPA for the admin area
  '/checkout/**': { ssr: true },              // always-fresh SSR
}

swr (stale-while-revalidate) and isr (incremental static regeneration) generate a page statically then refresh it in the background — ideal for high page-count e-commerce or news where a full rebuild on every change isn’t practical. Nuxt Islands (<NuxtIsland>) render components without shipping client-side JavaScript, cutting hydration cost and helping INP — the Core Web Vital that’s most often the problem on Nuxt apps.

How to verify what you actually shipped: View Source shows the raw HTML the server sent; if your content is there, you’re server-rendering. DevTools’ Elements panel shows the rendered DOM. And GSC’s URL Inspection tool shows what Google actually fetched and rendered — the source of truth. Don’t trust “it looks fine in my browser.”

How Googlebot handles a Nuxt app

Google processes any JavaScript app in three phases — crawl, render, index — and the catch is timing: rendering happens in a queue, not instantly. As I put it in my JavaScript SEO guide, the renderer is patient — “there is no fixed timeout for the renderer… It’s really patient, and you should not be concerned.” But patient isn’t the same as fast for fresh content. If you ship a client-rendered page, your content doesn’t exist for Google until that render wave runs; SSR and SSG close that gap because the HTML is complete on the first fetch.

Two more things matter at scale. Rendering JavaScript is expensive — from my 2019 JavaScript SEO talk (Ungagged), crawl costs go up by roughly 20× once Google has to render (directional, but the order of magnitude still holds). And Google takes the most restrictive directive across raw and rendered HTML — so a noindex injected by JavaScript will win over an index in the raw HTML, and vice versa. A canonical injected via JavaScript is respected only if there’s no canonical in the raw HTML already. Keep your robots and canonical signals in the server-rendered HTML, which Nuxt does for you when SSR is on.

The AI-crawler reality

This is the 2026 wrinkle. AI crawlers — GPTBot, ClaudeBot, PerplexityBot, and the rest — generally don’t execute JavaScript at all. They index the raw HTML and nothing else. So a client-rendered Nuxt page is effectively invisible to AI answer engines. SSR or SSG isn’t just better for Google here; it’s the price of entry for answer engine optimization too. If you want your content cited by ChatGPT, Perplexity, or Claude, it has to be in the HTML on first fetch.

Beyond the initial HTML: payload, hydration, and status codes

Getting server HTML with your content in it is necessary but not sufficient — several things can still go wrong downstream of that first response, and “I checked View Source” doesn’t cover them:

  • Payload and hydration. Universal rendering sends the HTML and a serialized data payload the client uses to hydrate — attach event listeners and pick up where the server left off — without re-fetching. Server HTML looking right doesn’t prove hydration succeeded, that client navigation reproduces the same content, or that the payload isn’t stale. If a page feels fine on first load but breaks after a client-side route change, that’s a hydration/payload problem, not a rendering-mode problem.
  • <ClientOnly> and browser-only content. Wrapping something in <ClientOnly> — common for browser-API-dependent widgets — means it’s absent from the server response even on an otherwise-universal route. If your main content, a key link, or your meta tags end up inside a client-only boundary, crawlers and AI bots that only read the raw HTML miss it, regardless of your rendering mode setting. Inspect the actual response, not just the rendering mode config.
  • Status codes and redirects aren’t self-certifying. A Nuxt error page rendering in the browser, or a navigateTo()/composable-driven redirect, doesn’t by itself prove what HTTP status the direct server response sent. Google’s guidance is explicit that meaningful status codes matter for crawling and indexing — confirm the actual header with curl -I, not what the client-rendered error page displays.

None of this is an argument against universal rendering — it’s the reminder that “the HTML is server-rendered” is the first check, not the last one.

Nitro, deployment presets, and cache boundaries

Nuxt’s server output is built by Nitro, and Nitro compiles differently depending on the deployment preset you target (Node server, Cloudflare, Vercel, Netlify, static, and others). That matters for SEO because presets and adapters can differ in runtime APIs available, caching behavior, streaming support, regional deployment, and filesystem access — a route rule or server handler that works under one preset isn’t guaranteed to behave identically under another. Two consequences worth testing explicitly rather than assuming:

  • Cache keys and invalidation are route-specific, not automatic. swr and isr route rules cache and regenerate output, but a wrong cache key, missing invalidation, or a caching header set by your deployment platform can serve stale, personalized, or inconsistent HTML to crawlers. Check the actual response age and any Cache-Control/Age headers on a live URL, not just the routeRules config.
  • Production parity isn’t guaranteed by staging behavior. A route rendering correctly in local dev or a preview deployment doesn’t confirm the production preset produces the same output — server routes, redirects, and error handling live in Nitro’s server layer, and that layer is the part most likely to differ by target. Test the production URL directly after any deployment change, the same way you’d verify rendering mode.

Meta tags: useSeoMeta() and useHead()

Nuxt’s head management runs on Unhead, and it gives you two composables for different jobs.

useSeoMeta() is the one to reach for for SEO and social meta tags. It’s a flat, type-safe API with typed parameters. Evidence for this claim useSeoMeta is a typed Nuxt API for SEO and social meta tags. Scope: Current Nuxt composable. Confidence: high · Verified: Nuxt: useSeoMeta It helps avoid the classic Open Graph bug of using name where you needed property:

useSeoMeta({
  title: 'My Page Title',
  ogTitle: 'My Page Title',
  description: 'Concise page-specific description with the key information first',
  ogDescription: 'Concise social description tailored to this page',
  ogImage: 'https://mysite.com/og-image.png', // must be an absolute URL
  twitterCard: 'summary_large_image',
})

useHead() is the general-purpose head tool for everything else — scripts, link tags, body attributes, and title templates:

useHead({
  titleTemplate: '%s · My Site Name',
  htmlAttrs: { lang: 'en' },
})

The reliable layering pattern is: static defaults (charset, viewport, favicon) in nuxt.config.ts → site-wide title template and global OG defaults in app.vue → page-specific overrides via useSeoMeta() in the page component. A common bug is putting useSeoMeta() in a layout instead of the page, which overwrites specific page tags with generic ones. And since search engines read the initial load, SEO meta generally doesn’t need to be reactive — useServerHead() skips the client-side re-execution.

Which API, and what it actually proves:

APIScopeReactive?What it proves about output
useSeoMeta()Flat, typed SEO/social meta onlyYes (default)Sets typed properties correctly — not that the route is unique, canonical, or indexable; you still own that logic
useHead()Anything in <head> — scripts, links, attrs, title templateYes (default)General head control; same caveat — API use isn’t proof of a correct server response
useServerHead() / server-only callsSame as aboveNo — server only, skips client re-executionConfirms the tag ships once in server HTML; doesn’t confirm client navigation re-sets it if you rely on it there

Calling one of these composables tells you the API ran — it doesn’t by itself tell you what a direct server response or a client-side route change actually emits. Confirm with View Source or curl, not just “I called useSeoMeta().”

The @nuxtjs/seo module ecosystem (Harlan Wilton) — optional, not core

Nuxt core doesn’t ship a sitemap, a robots.txt, OG image generation, or Schema.org out of the box — those are outside Nuxt’s own primitives (useHead, useSeoMeta, route rules, rendering modes). The community fills that gap with Harlan Wilton’s @nuxtjs/seo — a separately-installed, third-party umbrella package (nuxtseo.com, currently at v5.x, actively maintained, targeting Nuxt 3.16+ and Nuxt 4) that bundles six modules. Installing it gives you sitemap, robots, OG-image, and schema generation — it doesn’t by itself guarantee the output is correct for your routes; verify what it produces the same way you’d verify anything else:

ModuleWhat it does
@nuxtjs/robotsrobots.txt + meta robots + X-Robots-Tag headers
@nuxtjs/sitemapAuto XML sitemaps from pages + dynamic routes
nuxt-og-imageDynamic OG images (a Vue template → an image)
nuxt-schema-orgSchema.org JSON-LD structured data
nuxt-seo-utilsCanonical URLs, breadcrumbs, defaults
nuxt-link-checkerBuild-time broken-link detection

Install the whole bundle with npx nuxt module add seo, or grab individual modules (npx nuxt module add sitemap robots). A few behaviors worth knowing:

  • @nuxtjs/sitemap auto-generates from your pages/ directory plus dynamic routes, splits into a sitemap index automatically past 50,000 URLs, supports i18n multi-language sitemaps, and has built-in IndexNow support. One thing to get right: Google ignores changefreq and priority; only an accurate lastmod matters, and only when content genuinely changes.
  • @nuxtjs/robots generates robots.txt, the robots meta tag, and the X-Robots-Tag header — and by default it disallows all crawlers on non-production environments, which is exactly the staging-indexation footgun that bites headless builds. It also gives you per-bot rules, so you can block GPTBot specifically while leaving everyone else alone (be intentional — block an AI crawler and it won’t cite you).
  • nuxt-seo-utils handles canonical URLs and, usefully, strips tracking parameters (utm_*, fbclid, gclid) from canonicals automatically.

nuxt/image and Core Web Vitals

@nuxt/image is the image module: automatic responsive srcset, modern formats (WebP/AVIF), built-in optimization through CDN providers, and lazy loading. Two rules carry most of the SEO value:

  • Never lazy-load the LCP image. Your hero image should load eagerly; lazy-loading it delays your Largest Contentful Paint.
  • Always set width and height so the browser reserves space and you don’t take a Cumulative Layout Shift hit.
<NuxtImg
  src="/hero.jpg"
  width="1200"
  height="630"
  alt="Descriptive alt text"
  :loading="isHeroImage ? 'eager' : 'lazy'"
  format="webp"
/>

The 2026 targets to aim for (p75): LCP ≤ 2.5s, INP ≤ 200ms, CLS ≤ 0.1. On Nuxt apps, INP is the one that tends to suffer because hydration creates input lag — which is exactly what Nuxt Islands and <NuxtIsland> are for.

Nuxt Content + SEO

If you’re using @nuxt/content (the markdown/MDX content module), the integration with @nuxtjs/seo is straightforward but has one ordering gotcha: load @nuxtjs/seo before @nuxt/content in your modules array. You can then set SEO in content frontmatter (title, description, robots, ogImage, schemaOrg) and pull it into your [slug].vue template with useSeoMeta() after fetching the content. This is the Nuxt-native version of the headless-CMS pattern, and the same “rebuild what the plugin did” discipline applies.

Common Nuxt SEO mistakes

  1. Using SPA mode (ssr: false) for content you want indexed. The most expensive mistake — and the one that makes you invisible to AI crawlers.
  2. Relative URLs for OG images. ogImage must be an absolute URL or social previews break.
  3. useSeoMeta() in a layout instead of the page — generic tags overwrite page-specific ones.
  4. Not verifying rendered HTML — View Source ≠ DevTools ≠ what Google rendered. Use URL Inspection.
  5. Blocking JS/CSS in robots.txt — Google won’t render from blocked files.
  6. Leaving the staging noindex/disallow in place after launch (or, conversely, forgetting @nuxtjs/robots blocks non-prod by default and wondering why prod is fine but a custom env isn’t).
  7. Lazy-loading the LCP image — tanks your LCP.
  8. Missing width/height on images — CLS.
  9. changefreq/priority in sitemaps — Google ignores them; only lastmod counts.
  10. Reaching for dynamic rendering. Google deprecated it — “dynamic rendering is a workaround and not a long-term solution” — and it only serves the engines you configure it for, missing Bing and every AI crawler. With Nuxt you never need it: SSR and SSG already give crawlers complete HTML.

llms.txt and AEO

llms.txt is a plain-text file — robots.txt’s cousin — that helps AI tools navigate your content. The nuxt-llms module auto-generates /llms.txt and /llms-full.txt from Nuxt Content. As of late 2025 its primary consumers are MCP servers and AI coding tools (Cursor, Claude Code) rather than ChatGPT or Perplexity directly, and it’s most useful for documentation sites and technical blogs — less so for e-commerce or news. Worth adding if you’re a docs/dev site; not a priority otherwise.

Where this fits

Nuxt SEO is really a concrete application of JavaScript SEO through Nuxt’s own conventions — and it overlaps heavily with the SEO for a headless CMS topic when your Nuxt frontend pulls from a headless backend. The principles don’t change; Nuxt just gives you good defaults and a strong module ecosystem to implement them with.

Add an expert note

Pin an expert quote

New person? Create their unclaimed profile at /admin/experts/ → Pin an expert quote first.