Setting canonical URLs in Vue 3 composition API
Problem Statement
Client-side rendered (CSR) Vue 3 applications bypass traditional server-side meta injection. When Vue Router executes pushState navigation, the URL bar updates, but the DOM does not reload. Search engine crawlers initially parse the static index.html and only execute JavaScript after the hydration phase. If the <link rel="canonical"> tag remains static or updates asynchronously, Googlebot indexes the base URL for all route variations. This makes duplicate URLs compete against each other, splits link equity, and triggers parameter-based indexing bloat. For foundational head management patterns in SPAs, consult Vue Router Dynamic Meta Tags Setup. Without a deterministic, synchronous canonical injection strategy, dynamic routes will compete against each other in SERPs, degrading crawl budget efficiency and organic visibility.
Step-by-Step Fix
- Initialize History Mode: Configure Vue Router 4 with
createWebHistory()to generate clean, slash-delimited paths that match canonical targets. - Isolate Meta Logic: Extract canonical management into a dedicated
useCanonicalcomposable to prevent prop-drilling and maintain component purity. - Reactive Route Watching: Implement a
watchlistener onroute.paththat triggers on every client-side transition. - Absolute URL Construction: Concatenate
window.location.originwith the normalized route path. Relative canonicals are ignored by major crawlers. - Synchronous DOM Mutation: Query the existing
<link rel="canonical">element and update itshrefattribute directly. Avoiddocument.createElementon each navigation to prevent duplicate tag injection. - Lifecycle Cleanup: Call the watcher’s stop function in
onUnmounted. This prevents memory leaks during hot-module replacement (HMR) and route teardown.
Crawl/Index Impact: Synchronous DOM updates guarantee that the canonical tag is present before the JavaScript execution timeout expires. This aligns with Google’s rendering pipeline and ensures self-referencing canonicals are indexed correctly.
Validation
- DevTools Elements Inspection: Navigate between dynamic routes in Chrome. Verify
<link rel="canonical">updates instantly without page reload. - MutationObserver Console Test: Execute
new MutationObserver(m => console.log(m[0].target.href)).observe(document.querySelector('link[rel="canonical"]'), { attributes: true })in the console. Confirms reactive DOM updates fire on route change. - GSC URL Inspection API: Submit the dynamic route. Pass criteria:
User-declared canonicalmatches the renderedhrefexactly. Reject ifGoogle-selected canonicaldiffers. - Lighthouse SEO Audit: Run
lighthouse --only-categories=seo. Pass criteria: 0 “Duplicate canonical” warnings, 100/100 SEO score for meta tags.
Crawl/Index Impact: Headless validation confirms the tag survives hydration and matches the indexing pipeline. GSC verification proves Google respects the injected value over heuristic selection.
Code/Config
src/composables/useCanonical.ts
import { watch, onUnmounted } from 'vue';
import { useRoute } from 'vue-router';
export function useCanonical() {
const route = useRoute();
// Strip tracking parameters, normalize trailing slashes, enforce lowercase
const normalizeUrl = (path: string) => {
const cleanPath = path.replace(/\/+$/, '').toLowerCase();
return `${window.location.origin}${cleanPath}`;
};
const canonicalLink = document.querySelector<HTMLLinkElement>('link[rel="canonical"]');
const stopWatcher = watch(
() => route.path,
(newPath) => {
if (!canonicalLink) return;
canonicalLink.href = normalizeUrl(newPath);
},
{ immediate: true }
);
onUnmounted(() => {
stopWatcher();
});
}
src/views/ProductView.vue
Mount the composable inside a component’s setup() — not in a navigation guard — so that Vue’s lifecycle context is available.
<script setup lang="ts">
import { useCanonical } from '@/composables/useCanonical';
// Runs inside component setup(), where the composition context is active
useCanonical();
</script>
<template>
<main><!-- product content --></main>
</template>
src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{ path: '/products/:id', component: () => import('@/views/ProductView.vue') }
]
});
export default router;
index.html
<head>
<!-- Static fallback for initial SSR/crawler parse -->
<link rel="canonical" href="https://yourdomain.com/" />
</head>
Architecture Note: Call useCanonical() inside each route component’s setup() — never inside router.afterEach() or other navigation guards, where Vue’s composition API context is not available. The normalizeUrl function prevents query-string duplication and case-sensitivity indexing errors. The composable aligns with broader Framework-Specific SEO Implementations standards by decoupling SEO logic from UI components.
FAQ
Does Vue 3 Composition API automatically handle canonical tags?
No. Vue 3 does not inject or manage canonical tags by default. You must implement a reactive composable, or use a head management library such as @unhead/vue, to update the DOM synchronously on route changes.
How do I prevent duplicate canonical tags during client-side navigation?
Declare a single <link rel="canonical"> element in index.html and update its href attribute reactively via a watch on the router. Avoid document.createElement inside navigation hooks, as it appends duplicate tags that confuse crawlers.
Should canonical URLs include query strings in Vue SPAs? Generally, no. Strip tracking parameters (UTMs, session IDs, analytics tokens) and only preserve SEO-relevant query strings. Including transient parameters creates self-referencing duplicate content issues and wastes crawl budget.
Why can’t I call useCanonical() inside router.afterEach()?
Vue’s composition API functions (watch, onUnmounted, etc.) require an active component instance as context. Navigation guards run outside any component, so calling composables there throws a runtime error. Mount composables inside setup() instead.
Related
- Vue Router Dynamic Meta Tags Setup — the broader Vue head-management guide.
- Canonical URL Management in SPAs — the framework-agnostic canonical strategy.
- Fixing duplicate canonical tags in SPAs — the single-tag manager to reuse.
← Back to Vue Router Dynamic Meta Tags Setup