How to Debug Vue SEO Issues
- View Source shows what Google sees first. Inspect Element shows post-JavaScript DOM
- Inspect hydration warnings and actual head values; a DOM mismatch alone does not establish a metadata defect
- A successful URL Inspection live test shows tested rendering; stored indexed information is a separate observation
View Source shows the initial HTML. Google can also render JavaScript. Compare initial and rendered output, setup, data loading, and route configuration 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 absent there, check whether it appears after rendering or never loads.
<!-- ❌ BAD: Empty in View Source (SPA without SSR) -->
<!DOCTYPE html>
<html>
<head>
<title>Loading...</title>
</head>
<body>
<div id="app"></div>
<script src="/app.js"></script>
</body>
</html>
<!-- ✅ GOOD: Content in View Source (SSR) -->
<!DOCTYPE html>
<html>
<head>
<title>How to Debug Vue SEO | MySite</title>
<meta name="description" content="Fix meta tags not rendering in Vue apps...">
</head>
<body>
<div id="app">
<h1>How to Debug Vue SEO</h1>
<p>Fix meta tags not rendering...</p>
</div>
<script src="/app.js"></script>
</body>
</html>Google processes pages in three phases: crawling, rendering, and indexing. A page that needs JavaScript to show its content has to wait for the rendering phase before Google can index it, and Google publishes no fixed timeline for that wait. Initial HTML exposes content before 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 Unhead.
<!-- Direct title mutation bypasses Unhead -->
<script setup lang="ts">
import { watch } from 'vue'
import { useRoute } from 'vue-router'
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(). These setup excerpts require installed Unhead and Vue Router plugins, with a string route.meta.title on each page.
<!-- ✅ Updates on every navigation -->
<script setup lang="ts">
import { useHead } from '@unhead/vue'
import { useRoute } from 'vue-router'
const route = useRoute()
useHead({
title: () => route.meta.title as string
})
</script>Unhead synchronizes meta tags across SSR and client-side navigation. The Unhead documentation explains reactivity patterns.
Open Chrome DevTools while navigating between pages, and watch the <head> section in Elements tab:
- Open DevTools (F12)
- Select Elements tab
- Find
<head>element - Navigate to different page
- Watch for meta tag updates
If tags do not change, inspect reactive inputs, data, conflicting entries, and navigation errors. The symptom alone does not identify one cause.
Inspect Hydration Warnings and Head Output
During hydration, Vue attaches reactivity and event listeners to server-rendered HTML. A hydration mismatch occurs when the HTML rendered on the client differs from the server-rendered HTML.
Vue 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 trigger browser correction before hydration. Compare 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">
import { onMounted, ref } from 'vue'
const userAgent = ref('')
onMounted(() => {
userAgent.value = window.navigator.userAgent
})
</script>Values computed during rendering can differ between server and client:
<!-- ❌ Different on server vs client -->
<script setup lang="ts">
const timestamp = new Date().toLocaleString()
</script>
<!-- ✅ Use v-if + onMounted -->
<script setup lang="ts">
import { onMounted, ref } from 'vue'
const randomId = ref('')
const mounted = ref(false)
onMounted(() => {
mounted.value = true
randomId.value = Math.random().toString()
})
</script>
<template>
<div v-if="mounted">
ID: {{ randomId }}
</div>
</template>Third-party libraries may depend on browser APIs. Inspect SSR support and avoid server imports with browser-only side effects. Plain Vue does not provide a built-in ClientOnly component; supply an explicit client-only integration if needed.
Suppressing Mismatches in Vue 3.5+
For an expected rendered DOM difference, follow Vue’s data-allow-mismatch documentation. Its documented values are categories or an empty attribute, not a boolean true binding. Suppression does not correct inconsistent data or establish valid metadata.
Compare deployed HTML transformations and asset versions with the local response before blaming a minifier or browser cache. Do not disable services based on a warning alone.
Unhead Configuration Mistakes
Check plugin installation, reactive inputs, and the response integration separately.
Missing createHead() Setup
Unhead requires initialization. With Unhead 3.4.2, calling useHead without its Vue plugin can throw a missing-context error. It does not silently establish a head entry.
// ❌ Missing Unhead setup
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')Fix: Call createHead() and install plugin (Unhead Installation Guide):
// ✅ Correct Unhead setup
import { createHead } from '@unhead/vue/client'
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
const head = createHead()
app.use(head)
app.mount('#app')For an SSR app’s server entry, create a server head per request and install it before component setup. Render its tags into the returned HTML. The client entry needs its own client head during hydration. Follow the complete Vite SSR guide; changing an import alone does not establish SSR.
Wrong Import Paths (Unhead v2)
Current Unhead 3.4.2 exposes createHead through environment subpaths:
// Browser entry
import { createHead } from '@unhead/vue/client'// Server entry
import { createHead } from '@unhead/vue/server'These are separate entry-point references. Install the plugin in each corresponding app instance. Older release imports differ; use the guide and exports for the version you installed.
Not Using SSR
A client head can update the browser DOM. To place those dynamic tags in an initial HTML response, use an SSR/SSG response pipeline and serialize the head output. Static HTML can also supply tags. Google can render client JavaScript; a client-only head is not automatically invisible to Search. Follow the complete rendering setup, not an import-only switch.
Reactive Values Not Updating
Passing .value instead of the ref breaks reactivity:
<script setup lang="ts">
import { useHead } from '@unhead/vue'
import { ref } from 'vue'
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">
import { useHead } from '@unhead/vue'
import { ref } from 'vue'
const post = ref({ title: 'Loading...' })
useHead({
title: () => post.value.title // Updates when post changes
})
</script>Duplicate Meta Tags
Unhead deduplicates matching tags. Multiple entries can still conflict; inspect which value survives:
<!-- Matching descriptions are deduplicated -->
<script setup lang="ts">
import { useHead } from '@unhead/vue'
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 a single useHead() call per component.
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 registration, loaded data, and the response pipeline. 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 affected setup or head updates. Console shows Vue warnings and Unhead errors.
Look for:
- "Hydration completed but contains mismatches"
- "Unhead is missing Vue context"
- Failed fetch requests preventing data load
- Component errors during SSR
Fix errors before debugging meta tags. Inspect the failing setup or navigation before choosing a repair.
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
Basic audits do not prove metadata describes the page accurately. Review its actual values.
Google Search Console URL Inspection
URL Inspection distinguishes stored indexed information from a current live test.
How to Use URL Inspection
- Open Google Search Console
- Enter URL in search bar at top
- Click "Test Live URL" for current version
- View results
Inspect stored indexing state and crawl details separately from Test live URL. For a successful live test, open View tested page for tested HTML and a screenshot. A live success does not establish indexing. Follow Google’s URL Inspection documentation.
View Crawled Page
Read 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; they do not establish one rendering defect.
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. The live-test inspection limits do not establish the index-request scope. The Indexing API only covers JobPosting or livestream VideoObject containing BroadcastEvent. Its initial testing quota is 200 publish requests per project per day; it is not a general bulk-indexing route.
Common Vue 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.
Test:
curl -s https://yoursite.com | grep "<title>"If curl shows an empty title, inspect registration, data loading, and rendering configuration. If the route needs initial HTML, check its response pipeline.
Conditional repair: use SSR, SSG, or static tags when the route needs initial HTML. See Vue rendering modes guide.
Meta Tags Don't Update on Navigation
Possible causes: snapshot values, conflicting entries, failed navigation, or direct title mutation. This example requires installed plugins and string route.meta.title values.
Test: Navigate between pages, watch <title> in DevTools Elements tab.
Fix:
<script setup lang="ts">
import { useHead } from '@unhead/vue'
import { useRoute } from 'vue-router'
const route = useRoute()
useHead({
title: () => route.meta.title as string
})
</script>Google Shows Wrong Title/Description
Google can generate title links and snippets from visible content. A changed result does not prove an implementation failure. Compare the response title, rendered head, visible heading, and search result.
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
Inspect the actual response, rendered head, and warning before attributing a mismatch to metadata. Use stable page data for head values. Vue’s DOM mismatch suppression does not establish correct head output.
Content Loads After SSR
A client-only fetch starts after mounting, so its data is absent from the initial response. Missing data or failed setup can produce a similar symptom. A relative URL is not automatically an app URL in Node.
Use the initial catalog rendering excerpt with app-owned server-loaded props, and the complete Vite SSR setup for entries and response serialization. Preserve error and missing-data distinctions. Loading text does not expose the actual content.
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 the response pipeline 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 response 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 running site. A minimum SEO score does not prove every metadata value or indexing outcome.
Nuxt enables SSR and integrates a head manager. Data, reactive values, and deployment responses still need verification. See the Nuxt SEO debugging guide for framework-specific fixes.
Checklist
- When the route needs initial HTML, verify its content and tags in View Source
createHead()is set up and installed withapp.use(head), using@unhead/vue/serverfor SSR builds- Titles and descriptions use
useHead(), neverdocument.title - When tags need initial HTML, load their data before server rendering
- No "Hydration completed but contains mismatches" warnings in the console
- Compare initial responses, successful live tests, and stored indexed information without treating them as identical observations