---
title: "Dynamic Routes in Vue for SEO"
description: "Match Vue Router parameters, read reactive page data, and verify dynamic titles, canonicals, missing resources, and actual navigation."
canonical_url: "https://nuxtseo.com/learn-seo/vue/routes-and-rendering/dynamic-routes"
last_updated: "2026-10-05"
---

> Index of every page on this site: [llms.txt](https://nuxtseo.com/llms.txt)

::key-takeaways
- Dynamic route patterns match URLs. They do not generate published pages or prove that a record exists
- Read current route values and page data through reactive getters so metadata follows navigation
- Keep meaningful query values. Canonicalize only duplicate or near-identical alternatives under an explicit policy
::

Vue Router's [dynamic route matching](https://router.vuejs.org/guide/essentials/dynamic-matching) maps parameterized URLs to components. `/blog/:slug` can match `/blog/vue-guide`; your data layer still determines whether that post exists.

A path and a query string can both identify useful content. Neither route syntax nor head metadata guarantees indexing. Verify the actual response, data, links, and rendered output.

## Route Params vs Query Parameters

Both `/products/electronics/laptop` and `/products?category=electronics&item=laptop` can identify a product. [Google recommends readable URLs](https://developers.google.com/search/docs/crawling-indexing/url-structure), rather than an automatic ranking advantage for a path.

Distinguish tracking values from parameters that change the content. Read the [query parameter guide](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/query-parameters) before discarding filters or pagination from canonicals.

## Basic Dynamic Routes

These route excerpts assume an installed Vue Router app and your imported page components:

```ts
const routes = [
  { path: '/blog/:slug', name: 'BlogPost', component: BlogPost },
  { path: '/products/:category/:id', name: 'Product', component: Product }
]
```

They match `/blog/vue-guide` and `/products/electronics/123`. A component can reuse the same instance when only the parameter changes. Avoid copying a parameter into a plain setup-time constant when later navigation must update it.

## Per-Route Meta Tags

Use metadata that describes the current content. [Route meta fields](https://router.vuejs.org/guide/advanced/meta) store application data; they do not write document tags by themselves. Install [Unhead and its Vue plugin](https://unhead.unjs.io/docs/vue/head/guides/get-started/installation) before calling head composables.

### Static Meta for Route Groups

A route can define a default title. Read it from a component-setup getter in an installed router and head context:

```ts
import { useSeoMeta } from '@unhead/vue'
import { useRoute } from 'vue-router'

const route = useRoute()
useSeoMeta({
  title: () => typeof route.meta.title === 'string' ? route.meta.title : 'MySite'
})
```

A group default does not identify a particular post. Supply the current post's title where that distinction matters.

### Dynamic Meta from Data

This complete component uses two illustrative posts. Save it as `BlogPost.vue`. It assumes the router's `/blog/:slug` record and installed Unhead plugin. Replace the in-memory data with your app's loader and initial-state integration:

```vue
<script setup lang="ts">
import { useHead, useSeoMeta } from '@unhead/vue'
import { computed } from 'vue'
import { RouterLink, useRoute } from 'vue-router'

const posts = new Map([
  ['vue-guide', { title: 'Vue Guide', excerpt: 'Plan a Vue application.' }],
  ['router-guide', { title: 'Router Guide', excerpt: 'Choose routes and links.' }]
])
const route = useRoute()
const slug = computed(() => typeof route.params.slug === 'string' ? route.params.slug : '')
const post = computed(() => posts.get(slug.value))

useSeoMeta({
  title: () => post.value?.title ?? 'Post not found',
  description: () => post.value?.excerpt,
  robots: () => post.value ? 'index, follow' : 'noindex, follow'
})
useHead(() => ({
  link: post.value
    ? [{ rel: 'canonical', href: `https://example.com/blog/${slug.value}` }]
    : []
}))
</script>

<template>
  <article v-if="post">
    <h1>{{ post.title }}</h1>
    <p>{{ post.excerpt }}</p>
    <nav aria-label="Other posts">
      <RouterLink to="/blog/vue-guide">
        Vue Guide
      </RouterLink>
      <RouterLink to="/blog/router-guide">
        Router Guide
      </RouterLink>
    </nav>
  </article>
  <main v-else>
    <h1>Post not found</h1>
  </main>
</template>
```

This sample assumes query values do not change post content. It omits them from the canonical deliberately. A component's missing view and noindex tag cannot change an HTTP response already sent. Return a real 404 for missing records through the [host or server boundary](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/404-pages).

For SSR or generation, resolve required data before rendering. A browser-only loader can still produce content that Google renders, but the initial response may contain a loading view. Use the [complete Vite SSR setup](https://nuxtseo.com/learn-seo/vue/ssr-frameworks/vite-ssr) for routing, head serialization, client entries, and initial state.

## Multiple Route Params

This route excerpt matches `/docs/getting-started/installation`:

```ts
const routes = [
  { path: '/docs/:category/:page', component: DocPage }
]
```

Read both current values when selecting the document. Parse parameter values at your data boundary; do not assume an arbitrary URL proves a valid document exists.

## Optional Params

This installed-router excerpt matches `/blog/vue-guide` and `/blog/tutorials/vue-guide`:

```ts
const routes = [
  { path: '/blog/:category?/:slug', component: BlogPost }
]
```

If both URLs represent the same post, choose an explicit canonical policy. If the category changes the content, retain that distinction. A default category should not silently merge different resources.

## Catch-All Routes

Vue Router 4 uses a named catch-all parameter. This excerpt assumes your imported NotFound component:

```ts
const routes = [
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: NotFound,
    meta: { notFound: true }
  }
]
```

The browser can render a missing-page component. Your server or host must still return the correct HTTP status for a direct request. Bare `*` is not the Vue Router 4 catch-all syntax.

## Programmatic Navigation

Use Vue Router's navigation API for actions. This component-setup excerpt assumes the named BlogPost and Product routes above:

```ts
import { useRouter } from 'vue-router'

const router = useRouter()

function viewPost(slug: string) {
  return router.push({ name: 'BlogPost', params: { slug } })
}

function viewProduct(category: string, id: number) {
  return router.push({ name: 'Product', params: { category, id } })
}
```

Named routes handle parameter encoding. Names alone do not provide generated TypeScript route types. For discovery links, render an anchor through RouterLink or a regular HTML link; a button action does not supply a link URL to a crawler.

## Common SEO Issues

### Issue 1: Duplicate Content from Query Params

Tracking values can create alternate URLs for the same post. Google's canonical selection does not imply that every variation becomes a separately indexed page. Align canonical tags and links with the chosen URL.

Keep content-changing query values. Do not block a tracking variant in robots.txt as a substitute for letting Google fetch its canonical annotation.

### Issue 2: Missing Titles on Dynamic Routes

A generic title can hide what a post covers. The component above reads the current post title through a getter. Avoid creating a new unmanaged head entry on every navigation guard invocation.

### Issue 3: Client-Side Rendering

Google can render JavaScript. Client-created content depends on successful execution and available resources. SSR or [generation](https://nuxtseo.com/learn-seo/vue/spa/prerendering) can put content in initial HTML; neither method guarantees indexing or a particular processing time.

### Issue 4: Infinite Parameter Variations

Filter combinations can create many unnecessary URLs, but distinct results are not automatically duplicates. Decide which combinations serve users, which need discovery, and which should be excluded.

[Noindex and crawl restrictions](https://nuxtseo.com/learn-seo/vue/controlling-crawlers) solve different problems. Do not noindex every URL merely because it has a query string. Google must fetch a page to read its noindex or canonical tag.

## Route Params Table

| Pattern                       | Matches              | Params                                 | Use Case            |
| ----------------------------- | -------------------- | -------------------------------------- | ------------------- |
| `/blog/:slug`                 | `/blog/vue-guide`    | `{ slug: 'vue-guide' }`                | Blog posts          |
| `/products/:id`               | `/products/123`      | `{ id: '123' }`                        | Product identifiers |
| `/docs/:category/:page`       | `/docs/api/methods`  | `{ category: 'api', page: 'methods' }` | Documentation       |
| `/user/:id(\\d+)`{lang="ts"}  | `/user/42`           | `{ id: '42' }`                         | Numeric identifiers |
| `/:lang/about`                | `/en/about`          | `{ lang: 'en' }`                       | Locale paths        |
| `/files/:path(.*)`{lang="ts"} | `/files/docs/api.md` | `{ path: 'docs/api.md' }`              | Nested path string  |

## Regex Constraints

These installed-router excerpts constrain matching. The double backslash escapes the JavaScript string:

```ts
const routes = [
  { path: '/user/:id(\\d+)', component: User },
  { path: '/blog/:slug([a-z0-9-]+)', component: BlogPost }
]
```

An unmatched URL can reach the catch-all view. Regex matching does not set a server status, establish resource existence, or prevent indexing by itself.

## Navigation Guards for Meta

Put a shared title template in the root component's head setup, then supply reactive page titles in page components. This root-setup excerpt assumes Unhead's installed plugin:

```ts
import { useHead } from '@unhead/vue'

useHead({ titleTemplate: '%s | MySite' })
```

If a guard needs to mutate head state outside component setup, use the explicit head instance according to Unhead's API. Do not assume a Vue-injected composable has component context inside an arbitrary callback.

## TypeScript Support

Vue Router's default params can be strings or arrays. Narrow the value you need, as the computed slug above does. `useRoute<BlogParams>()`{lang="ts"} does not install a parameter interface; its generic names a route.

For generated typed route maps, follow [Vue Router's typed routes documentation](https://router.vuejs.org/guide/advanced/typed-routes). RouteMeta module augmentation describes your route metadata, while URL data still needs parsing at the boundary.

## Verification

Check two known posts, then a missing slug. Direct requests should return the correct content and status. Inspect title, description, and canonical tags in initial and rendered HTML.

Follow an actual link between posts. Confirm both content and head tags update after navigation. A loading title alone does not identify the cause; inspect data loading, rendering, and errors.

Search Console's live URL test reports a fetch condition. Compare it with indexed crawl information rather than treating it as proof of indexing.

## Checklist

::checklist{#vue-dynamic-routes}
- Route patterns map valid URLs, and data lookup confirms the resource exists
- Current data drives titles and descriptions through reactive getters
- Required data reaches the SSR or generation step before rendering when clients need initial content
- Canonical policy retains meaningful query differences
- Missing resources return the correct status through the host or server
- Direct requests and real navigation show the expected content and metadata
::

Using Nuxt? File-based routing declares dynamic patterns and Nuxt supplies SSR integration. Your app still loads data, handles missing records, and sets appropriate metadata. [Learn more in Nuxt →](https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering)

## Sitemap

See the full [sitemap](https://nuxtseo.com/sitemap.md) for all pages.
