Index of every page on this site: llms.txt

How to Debug Nuxt SEO Issues

Fix meta tags that won't render, hydration mismatches, and stalled indexing in Nuxt using View Source, DevTools, and Search Console.
Harlan Wilton10 mins read Published Updated
What you'll learn
  • Use View Source to inspect initial HTML, then DevTools or URL Inspection to inspect rendered output
  • useFetch() and useAsyncData() work during SSR; onMounted() fetches are client-only
  • Successful URL Inspection live tests expose tested rendering; stored indexed information describes a different observation

View Source shows the initial HTML. Google can also render JavaScript, so compare both initial and rendered output. Inspect configuration, data loading, hydration, and the chosen rendering mode before identifying a cause.

View Source vs Inspect Element

Two different ways to view HTML show different content:

View Source (Right-click → View Page Source) shows the initial HTML your server sends. This is what Google's crawler sees first.

Inspect Element (F12 → Elements tab) shows the live DOM after JavaScript executes. Meta tags added by JavaScript appear here but might not be indexed.

Test: Open your site, right-click, select "View Page Source." Search for <title> or <meta name="description". If content is missing there, check whether it appears after rendering or never loads. Check the rendered page and any JavaScript failures.

<!-- ❌ BAD: Empty in View Source (SPA mode) -->
<!DOCTYPE html>
<html>
  <head>
    <title>Loading...</title>
  </head>
  <body>
    <div id="__nuxt"></div>
    <script src="/_nuxt/entry.js"></script>
  </body>
</html>

<!-- ✅ GOOD: Content in View Source (SSR mode, default) -->
<!DOCTYPE html>
<html>
  <head>
    <title>How to Debug Nuxt SEO | MySite</title>
    <meta name="description" content="Fix meta tags not rendering in Nuxt apps...">
  </head>
  <body>
    <div id="__nuxt">
      <h1>How to Debug Nuxt SEO</h1>
      <p>Fix meta tags not rendering...</p>
    </div>
    <script src="/_nuxt/entry.js"></script>
  </body>
</html>

Google processes pages in three phases: crawling, rendering, and indexing. A page that needs JavaScript to show its content must reach the rendering phase before Google can index it. Initial HTML exposes content without depending on JavaScript execution. Google does not publish a fixed rendering or indexing delay.

Meta Tags Not Updating on Navigation

Single Page Applications change routes without full page reloads. Meta tags set during initial render might not update when users navigate.

Common mistake: Using document.title instead of Nuxt's composables.

<!-- Direct title mutation bypasses the shared head manager -->
<script setup lang="ts">
const route = useRoute()

watch(() => route.path, () => {
  document.title = route.meta.title as string // Can change the browser title independently of Unhead
})
</script>

Fix: Use reactive inputs with useHead() or useSeoMeta(). These examples require each page to define a string route.meta.title.

<!-- ✅ Updates on every navigation -->
<script setup lang="ts">
const route = useRoute()

useHead({
  title: () => route.meta.title as string
})
</script>

Nuxt's Unhead integration synchronizes meta tags across SSR and client-side navigation. The Unhead documentation explains reactivity patterns.

Inspect the rendered head during navigation with browser DevTools:

  1. Open the app and browser DevTools
  2. Select the Elements panel and expand <head>
  3. Inspect the title and relevant meta tags
  4. Navigate through the app’s links and compare their values

If tags do not change, inspect reactive inputs, page data, conflicting head entries, and navigation errors. SSR mode alone does not identify the cause.

Inspect Hydration Warnings and Head Output

Hydration is when Vue takes server-rendered HTML and "activates" it with reactivity and event listeners. A hydration mismatch occurs when the HTML rendered on the client differs from the server-rendered HTML.

Nuxt displays warnings like "Hydration completed but contains mismatches" or "Text content does not match server-rendered HTML." These warnings appear in browser console and indicate server/client HTML differences (Vue SSR Guide).

Common Causes

Invalid HTML nesting can cause browser correction before hydration. Compare the actual response and parsed DOM against Vue’s hydration guidance.

Browser-only APIs don't exist during SSR:

<!-- ❌ Crashes during SSR -->
<script setup lang="ts">
const userAgent = window.navigator.userAgent
</script>

<!-- ✅ Only access on client -->
<script setup lang="ts">
const userAgent = ref('')

onMounted(() => {
  userAgent.value = window.navigator.userAgent
})
</script>

Random values and timestamps can differ between server and client. Serialize a shared value, or intentionally render client-only content when the reader task permits it. A static string does not demonstrate a random-value mismatch.

Third-party libraries can depend on browser-only APIs. Inspect their SSR support before using them on the server. This template requires an app-owned GoogleMap component:

<!-- Wrap client-only libraries -->
<template>
  <ClientOnly>
    <GoogleMap />
  </ClientOnly>
</template>

Suppressing Mismatches in Vue 3.5+

For a genuinely expected difference, follow Vue’s data-allow-mismatch documentation. Suppression does not correct inconsistent data or replace investigating an unexpected mismatch.

Check any HTML transformation service in your deployment. Compare its response with your local server before attributing a hydration mismatch.

Stale assets can occur across deploys. Compare response and asset versions before blaming the browser cache.

Nuxt Configuration Mistakes

Check the rendering and data configuration for the affected route.

Disabling SSR Globally

Setting ssr: false removes server-rendered content. Google can render JavaScript, but clients needing initial HTML may not see it:

// Disables server rendering globally
export default defineNuxtConfig({
  ssr: false
})

If a route needs content in its initial HTML, remove an unintended global SSR disable or use route-level rules:

// ✅ Disable SSR only where needed
export default defineNuxtConfig({
  routeRules: {
    '/dashboard/**': { ssr: false }, // Client-only for authenticated routes
    '/blog/**': { prerender: true } // SSG for blog pages
  }
})

Reactive Values Not Updating

Passing .value instead of the ref breaks reactivity:

<script setup lang="ts">
const pageTitle = ref('Loading...')

// ❌ Loses reactivity
useHead({
  title: pageTitle.value
})

// ✅ Stays reactive
useHead({
  title: pageTitle
})

// Later update works with reactive version
pageTitle.value = 'Loaded Content'
</script>

For computed values, use getter functions:

<script setup lang="ts">
const post = ref({ title: 'Loading...' })

useHead({
  title: () => post.value.title // Updates when post changes
})
</script>

Duplicate Meta Tags

Multiple head entries can conflict. Unhead deduplicates matching tags; inspect which value survives:

<!-- Conflicting descriptions: Unhead deduplicates matching tags -->
<script setup lang="ts">
useHead({
  meta: [{ name: 'description', content: 'First description' }]
})

useHead({
  meta: [{ name: 'description', content: 'Second description' }]
})
</script>

Unhead deduplicates by default but multiple calls can override unexpectedly. Consolidate meta tags into single useHead() or useSeoMeta() call per component.

Missing Data During SSR

These contrast excerpts require an app-owned /api/post endpoint and show initial-response timing only. They omit endpoint contracts and error/missing handling; use the complete dynamic-route pattern for a page implementation:

<!-- ❌ Client-only fetch -->
<script setup lang="ts">
const post = ref({ title: 'Loading...' })

onMounted(() => {
  (async () => {
    post.value = await fetch('/api/post').then(r => r.json())
  })()
})

useHead({ title: () => post.value.title })
</script>

<!-- ✅ SSR-compatible fetch -->
<script setup lang="ts">
const { data: post } = await useFetch('/api/post')

useHead({ title: () => post.value?.title || 'Loading...' })
</script>

Nuxt's useFetch() and useAsyncData() work during both SSR and client-side navigation.

Chrome DevTools SEO Workflow

DevTools helps debug meta tags, rendering issues, and JavaScript errors.

1. Check Initial HTML Response

Network panel shows server-sent HTML before JavaScript executes:

  1. Open DevTools (F12)
  2. Network tab → Clear (trash icon)
  3. Reload page
  4. Click first request (usually document)
  5. Preview or Response tab shows initial HTML

If tags are absent, inspect their registration, data state, and rendering configuration. Absence alone does not establish the cause.

To find all meta tags at once, switch to the Elements panel, press Ctrl+F (Cmd+F on Mac), and search for <meta name="description". Compare what you find against your useHead() calls.

2. Console Errors

JavaScript errors can interrupt meta tag updates. Console shows Vue warnings and Unhead errors.

Look for:

  • "Hydration completed but contains mismatches"
  • Failed fetch requests preventing data load
  • Component errors during SSR
  • Route errors breaking navigation

Fix errors before debugging meta tags. A JavaScript failure can interrupt the affected setup or navigation.

3. Lighthouse SEO Audit

Lighthouse tab includes basic SEO checks:

  1. Lighthouse tab
  2. Select "SEO" category
  3. Click "Generate report"

Checks:

  • Document has <title> element
  • Document has meta description
  • Links have descriptive text

These basic checks do not prove that metadata describes the page accurately. Review the values separately.

Google Search Console URL Inspection

URL Inspection distinguishes stored indexed information from a current live test.

How to Use URL Inspection

  1. Inspect the URL’s stored indexed state and crawl details.
  2. Run Test live URL to inspect its current response.
  3. For a successful test, open View tested page for tested HTML and screenshot.

A live test does not prove indexing. Compare the tested time, response, rendered content, and resource errors separately from the last indexed crawl.

View Crawled Page

Read the available stored crawl information for the indexed URL. A screenshot is available for a successful live test, not the indexed crawl. Differences from your browser can reflect data, timing, resources, or personalization.

JavaScript Rendering Test

Google processes pages in three phases: crawling, rendering, then indexing. Content that only shows up once JavaScript has rendered waits for that second phase before it can be indexed. URL Inspection shows both:

  • Successful live test → Tested current rendering
  • Stored indexed information → Available observations from Google’s recorded crawl

Request Indexing

After fixing issues:

  1. URL Inspection → "Request Indexing"
  2. Google queues the URL for recrawl

Google doesn't guarantee indexing or a specific timeline. Indexing remains a separate decision.

Google limits daily index requests. Live-test inspection limits do not establish the index-request scope. The Indexing API only covers JobPosting or VideoObject containing a livestream BroadcastEvent. It is not a general bulk-indexing route; quotas and eligibility still apply.

Common Nuxt SEO Debugging Scenarios

Meta Tags Appear in DevTools But Not View Source

Possible causes: client-only registration, delayed data, failed setup, or a client-rendered route. Inspect the response and rendered head before diagnosing.

Test:

curl -s https://yoursite.com | grep "<title>"

If curl shows an empty title, inspect registration, loaded data, and rendering rules. If the route needs initial HTML, check for unintended SSR disabling.

Conditional repair: if the route needs initial HTML and its configuration disables SSR unintentionally, enable server rendering for that route:

// nuxt.config.ts
export default defineNuxtConfig({
  // Remove ssr: false if present

  // Or use route-level control
  routeRules: {
    '/dashboard/**': { ssr: false }, // Only disable for client-only sections
  }
})

Meta Tags Don't Update on Navigation

Possible causes: snapshot values, conflicting entries, failed navigation, or direct title mutation. The following example requires string page titles in route.meta.title.

Test: navigate through app links and inspect <title> in the browser DevTools Elements panel.

Fix:

<script setup lang="ts">
const route = useRoute()

useHead({
  title: () => route.meta.title as string
})
</script>

Google Shows Wrong Title/Description

Behavior: Google can generate title links and snippets from page content. A changed result does not prove an implementation failure.

Test: compare the displayed result with the response title, rendered title, visible heading, and page content.

Fix: use descriptive titles and descriptions matching the page. Google does not prescribe universal 50–60 or 150–160 character limits.

Hydration Mismatch on Meta Tags

Possible cause: inconsistent server and client values. Inspect the actual response, rendered head, and warnings before attributing a mismatch to metadata.

Test: Look for console warnings: "Hydration completed but contains mismatches."

Fix: use stable page data for metadata. Vue’s DOM mismatch suppression does not establish correct head values. Use the linked Vue guidance only for an intentional rendered DOM difference.

Content Loads After SSR

Possible cause: data fetched in onMounted(). Failed setup and missing data can also leave an initial loading state.

Test: View Source shows "Loading..." instead of actual content.

Fix: use the complete dynamic-route pattern linked above. These timing fragments require the app-owned post endpoint and omit its data/error contract:

<!-- ❌ Client-only fetch -->
<script setup lang="ts">
const post = ref({ title: 'Loading...' })

onMounted(() => {
  (async () => {
    post.value = await fetch('/api/post').then(r => r.json())
  })()
})

useHead({ title: () => post.value.title })
</script>

<!-- ✅ SSR-compatible fetch -->
<script setup lang="ts">
const { data: post } = await useFetch('/api/post')

useHead({ title: () => post.value?.title || 'Loading...' })
</script>

Testing Before Deployment

Don't wait for Google to tell you something's broken. Test locally: right-click → View Page Source and search for critical meta tags, checking registration, data loading, and rendering configuration if they are missing.

1. Curl Test

# Check initial HTML
curl -s https://yoursite.com | grep -A 5 "<head>"

# Check specific meta tag
curl -s https://yoursite.com | grep 'name="description"'

Should show full meta tags in output. Private staging requires authentication. Use direct authorized inspection there; Search Console cannot fetch a private page without access.

2. Lighthouse CI

Use the Lighthouse CI getting-started procedure with an installed CLI and a running site. A minimum SEO score does not prove every metadata value or indexing outcome.

3. Inspect Deployed Output

Inspect deployed HTTP responses, the browser’s rendered head, and server logs. Compare those observations with local development. Enabling a config flag alone does not establish production DevTools availability.

Inspect the initial HTML for clients that do not execute JavaScript. Changing a browser's user agent does not disable JavaScript or prove how a vendor bot renders pages.

Checklist

Checklist
  • When the route needs initial HTML, verify its content and tags in View Source
  • No unintended ssr: false on public routes in nuxt.config.ts or routeRules
  • Titles and descriptions use useHead() or useSeoMeta(), never document.title
  • When tags need initial HTML, load their data through server-capable useFetch() or useAsyncData() before rendering
  • No "Hydration completed but contains mismatches" warnings in the console
  • Compare direct responses, successful live tests, and stored indexed state without treating them as identical observations
Was this page helpful?