How to Debug Nuxt SEO Issues
- Use View Source to inspect initial HTML, then DevTools or URL Inspection to inspect rendered output
useFetch()anduseAsyncData()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:
- Open the app and browser DevTools
- Select the Elements panel and expand
<head> - Inspect the title and relevant meta tags
- 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:
- Open DevTools (F12)
- Network tab → Clear (trash icon)
- Reload page
- Click first request (usually document)
- 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:
- Lighthouse tab
- Select "SEO" category
- 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
- Inspect the URL’s stored indexed state and crawl details.
- Run Test live URL to inspect its current response.
- 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:
- URL Inspection → "Request Indexing"
- 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.
Checklist
- When the route needs initial HTML, verify its content and tags in View Source
- No unintended
ssr: falseon public routes innuxt.config.tsorrouteRules - Titles and descriptions use
useHead()oruseSeoMeta(), neverdocument.title - When tags need initial HTML, load their data through server-capable
useFetch()oruseAsyncData()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