---
title: "Dynamic Routes in Vue for SEO · Nuxt SEO"
canonical_url: "https://nuxtseo.com/learn-seo/vue/routes-and-rendering/dynamic-routes"
last_updated: "2026-07-16T12:00:00.000Z"
meta:
  author: "Harlan Wilton"
  description: "Configure per-route meta tags in Vue Router's dynamic routes, and stop query parameters from creating duplicate content in Google's index."
  "og:description": "Configure per-route meta tags in Vue Router's dynamic routes, and stop query parameters from creating duplicate content in Google's index."
  "og:title": "Dynamic Routes in Vue for SEO · Nuxt SEO"
---

Nuxt SEO on GitHub

# **Dynamic Routes in Vue for SEO**

Configure per-route meta tags in Vue Router's dynamic routes, and stop query parameters from creating duplicate content in Google's index.

[Harlan Wilton](https://x.com/harlan-zw)12 mins read Published **Dec 17, 2025** Updated **Jul 16, 2026**

**What you'll learn**

- Dynamic params create semantic URLs. `**/blog/:slug**` instead of `**/blog?id=123**`
- Set per-route meta tags by fetching data before render, not in `**onMounted()**`
- Canonical URLs should strip query params to prevent duplicate content

Dynamic routes generate clean URLs from parameters. `**/blog/:slug**` creates `**/blog/vue-seo-guide**` instead of `**/blog?id=123**`. Search engines prefer semantic paths over query parameters.

Vue Router's [**~~dynamic route matching~~**](https://router.vuejs.org/guide/essentials/dynamic-matching) handles this automatically. Configure your routes once, set per-route meta tags, and Google indexes each page correctly.

## Route Params vs Query Parameters

Google treats these differently:

```
✅ /products/electronics/laptop
❌ /products?category=electronics&item=laptop
```

Route params create semantic URLs. Query parameters generate duplicate content issues. Google sees infinite URL variations when you add filters, sorting, or tracking params.

Google's [**~~URL structure guidelines~~**](https://developers.google.com/search/docs/crawling-indexing/url-structure) call for descriptive, readable URLs and as few parameters as possible. Route params satisfy that by design; query strings pile up into separate crawlable variations if you don't keep them minimal.

## Basic Dynamic Routes

Define routes with `**:param**` syntax:

```ts
import { createRouter, createWebHistory } from 'vue-router'

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

const router = createRouter({
  history: createWebHistory(),
  routes
})
```

This generates:

- `**/blog/vue-ssr-guide**`
- `**/products/electronics/123**`

Access params in components via `**route.params**`:

```vue
<script setup lang="ts">
import { useRoute } from 'vue-router'

const route = useRoute()
const slug = route.params.slug // "vue-ssr-guide"
</script>
```

## Per-Route Meta Tags

Search engines need unique titles and descriptions for each dynamic route. Set these with [**~~route meta fields~~**](https://router.vuejs.org/guide/advanced/meta.html) and Unhead.

### Static Meta for Route Groups

Define generic meta in route config:

```ts
const routes = [
  {
    path: '/blog/:slug',
    component: BlogPost,
    meta: {
      title: 'Blog',
      description: 'Read our latest articles'
    }
  }
]
```

Apply meta in a navigation guard:

```ts
router.afterEach((to) => {
  useHead({
    title: to.meta.title as string,
    titleTemplate: '%s | MySite'
  })
})
```

This works for basic cases but gives every blog post the same title ("Blog | MySite"). Override in components for dynamic content.

### Dynamic Meta from Data

Fetch data and set specific meta tags per page:

```vue
<script setup lang="ts">
import { useRoute } from 'vue-router'

const route = useRoute()
const post = await fetchPost(route.params.slug)

// Overrides route meta with actual post data
useHead({
  title: post.title, // "How to Build a Vue Blog"
  meta: [
    { name: 'description', content: post.excerpt }
  ]
})

// Or use useSeoMeta for SEO-specific tags
useSeoMeta({
  title: post.title,
  description: post.excerpt,
  ogTitle: post.title,
  ogDescription: post.excerpt,
  ogImage: post.coverImage
})
</script>

<template>
  <article>
    <h1>{{ post.title }}</h1>
    <p>{{ post.content }}</p>
  </article>
</template>
```

**SSR requirement:** Fetch data before render, not in `**onMounted()**`. Client-side fetches mean search engines see loading states. Use SSR-compatible patterns:

```ts
// server.ts
import express from 'express'
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'

const app = express()

app.get('/blog/:slug', async (req, res) => {
  const post = await fetchPost(req.params.slug)

  const vueApp = createSSRApp({
    setup() {
      useHead({
        title: post.title,
        meta: [{ name: 'description', content: post.excerpt }]
      })
    }
  })

  const html = await renderToString(vueApp)
  res.send(html)
})
```

Read the [**~~SSR rendering guide~~**](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/rendering) for full SSR setup.

## Multiple Route Params

Combine params for hierarchical URLs:

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

Generates: `**/docs/getting-started/installation**`

Access all params:

```vue
<script setup lang="ts">
const route = useRoute()
const { category, page } = route.params

// Fetch content based on both params
const doc = await fetchDoc(category, page)

useSeoMeta({
  title: doc.title,
  description: doc.excerpt
})
</script>
```

## Optional Params

Make params optional with `**?**`:

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

Matches both:

- `**/blog/vue-guide**` (category undefined)
- `**/blog/tutorials/vue-guide**` (category = "tutorials")

Handle undefined params in components:

```ts
const route = useRoute()
const category = route.params.category || 'general'
```

## Catch-All Routes

Use `*****` or `**:pathMatch(.*)***` for 404 pages:

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

Set appropriate meta for 404s:

```vue
<script setup lang="ts">
useHead({
  title: '404 - Page Not Found',
  meta: [
    { name: 'robots', content: 'noindex, nofollow' }
  ]
})
</script>
```

## Programmatic Navigation

Navigate to dynamic routes programmatically:

```vue
<script setup lang="ts">
import { useRouter } from 'vue-router'

const router = useRouter()

function viewPost(slug: string) {
  // Updates URL and triggers route meta/title updates
  navigateTo(`/blog/${slug}`)
}

function viewProduct(category: string, id: number) {
  navigateTo({
    name: 'Product',
    params: { category, id }
  })
}
</script>
```

Named routes prevent typos:

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

// Type-safe with route names
router.push({ name: 'Product', params: { category: 'electronics', id: 123 } })
```

## Common SEO Issues

### Issue 1: Duplicate Content from Query Params

Mixing route params and query params creates duplicates:

```
/blog/vue-guide
/blog/vue-guide?ref=twitter
/blog/vue-guide?utm_source=newsletter
```

Google indexes these as separate pages. Fix with canonical tags:

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

// Strip query params from canonical URL
const canonicalUrl = computed(() => {
  return `https://yoursite.com${route.path}`
})

useHead({
  link: [
    { rel: 'canonical', href: canonicalUrl.value }
  ]
})
</script>
```

Or block the pattern in `**robots.txt**`, per [**~~Google's URL structure guidelines~~**](https://developers.google.com/search/docs/crawling-indexing/url-structure):

```
User-agent: *
Disallow: /*?ref=*
Disallow: /*?utm_*
```

### Issue 2: Missing Titles on Dynamic Routes

Generic titles hurt SEO:

```html
<!-- ❌ Every blog post shows "Blog | MySite" -->
<title>Blog | MySite</title>
```

Always override with specific content:

```vue
<script setup lang="ts">
const post = await fetchPost(route.params.slug)

// ✅ Each post gets unique title
useHead({
  title: post.title // "How to Build a Vue Blog"
})
</script>
```

### Issue 3: Client-Side Rendering

SPA routing means search engines see your loading state:

```html
<!-- Google sees this if you fetch in onMounted() -->
<title>Loading...</title>
<h1>Loading...</h1>
```

Use SSR or [**~~prerendering~~**](https://nuxtseo.com/learn-seo/vue/spa/prerendering) to ship complete HTML. Google [**~~processes pages in three phases~~**](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics): crawling, rendering, and indexing. It publishes no timeline for the render step, so a page that depends on client-side JavaScript can sit unindexed longer than one that ships its content in the initial response.

### Issue 4: Infinite Parameter Variations

Dynamic routes with filters create crawl budget waste:

```
// ❌ Generates thousands of URLs
/products/:category?sort=price
/products/:category?sort=name
/products/:category?color=red&sort=price
// ... infinite combinations
```

Use `**noindex**` on filtered pages or block in `**robots.txt**`:

```ts
router.afterEach((to) => {
  // Noindex pages with query params
  if (Object.keys(to.query).length > 0) {
    useHead({
      meta: [
        { name: 'robots', content: 'noindex, follow' }
      ]
    })
  }
})
```

Read more about [**~~controlling crawlers~~**](https://nuxtseo.com/learn-seo/vue/controlling-crawlers).

## Route Params Table

Common dynamic route patterns:

| **Pattern** | **Matches** | **Params** | **Use Case** |
| --- | --- | --- | --- |
| `**/blog/:slug**` | `**/blog/vue-guide**` | `**{ slug: 'vue-guide' }**` | Blog posts, articles |
| `**/products/:id**` | `**/products/123**` | `**{ id: '123' }**` | Product pages |
| `**/docs/:category/:page**` | `**/docs/api/methods**` | `**{ category: 'api', page: 'methods' }**` | Documentation |
| `**/user/:id(\\d+)**` | `**/user/42**` | `**{ id: '42' }**` | User profiles (numeric IDs only) |
| `**/:lang/about**` | `**/en/about**` | `**{ lang: 'en' }**` | Internationalized routes |
| `**/files/:path(.*)**` | `**/files/docs/api.md**` | `**{ path: 'docs/api.md' }**` | Nested file paths |

## Regex Constraints

Validate params with regex:

```ts
const routes = [
  {
    // Only match numeric IDs
    path: '/user/:id(\\d+)',
    component: User
  },
  {
    // Only match valid slugs (lowercase, hyphens)
    path: '/blog/:slug([a-z0-9-]+)',
    component: BlogPost
  }
]
```

Invalid params result in 404, preventing indexing of malformed URLs.

## Navigation Guards for Meta

Set global meta patterns in guards:

```ts
router.beforeEach((to, from) => {
  // Set default meta for all routes
  useHead({
    titleTemplate: '%s | MySite',
    meta: [
      { property: 'og:site_name', content: 'MySite' },
      { name: 'twitter:card', content: 'summary_large_image' }
    ]
  })
})

router.afterEach((to) => {
  // Apply route-specific meta
  if (to.meta.title) {
    useHead({
      title: to.meta.title as string
    })
  }
})
```

Components can override guard defaults with their own `**useHead()**` calls. Unhead merges them with component-level taking precedence.

## TypeScript Support

Type route params and meta:

```ts
import 'vue-router'

declare module 'vue-router' {
  interface RouteMeta {
    title?: string
    description?: string
    requiresAuth?: boolean
  }
}

// Type-safe route params
interface BlogParams {
  slug: string
}

const route = useRoute<BlogParams>()
const slug: string = route.params.slug // Typed
```

## Verification

Check what Google indexes:

1. **View Page Source** (not Inspect Element): Right-click → View Page Source. Should show complete HTML with title and meta tags.
2. **Google Search Console URL Inspection**: Test Live URL → View HTML. If you see `**<title>Loading...</title>**`, your SSR isn't working.
3. **Curl test**:

```bash
curl https://yoursite.com/blog/vue-guide | grep "<title>"
```

Should return full title tag, not empty or "Loading".

## Checklist

**Checklist**

- Dynamic routes use `**:param**` syntax instead of query strings for canonical content
- Meta tags are set from fetched data with `**useHead()**` or `**useSeoMeta()**`, not left generic
- Data fetches happen before render (SSR-compatible), not in `**onMounted()**`
- Canonical URLs strip tracking and filter query params
- Regex constraints on route params keep malformed URLs out of the index
- View Source and Search Console's URL Inspection both show the real title, not a loading state

Using Nuxt? It handles dynamic routes automatically with file-based routing: `**pages/blog/[slug].vue**` creates the route, `**useSeoMeta()**` sets tags, and SSR works by default. [**~~Learn more in Nuxt →~~**](https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering)

[**The 2026 SEO Checklist for Nuxt & Vue ** Pre-launch setup, post-launch verification, and ongoing monitoring. Interactive checklist with links to every guide.](https://nuxtseo.com/learn-seo/checklist) [Haven't launched yet? Start with the **Pre-Launch Warmup**](https://nuxtseo.com/learn-seo/pre-launch-warmup)

---

### **Related **

[**URL Structure**](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/url-structure)

[**Page Titles in Vue**](https://nuxtseo.com/learn-seo/vue/mastering-meta/titles)

[**Canonical URLs**](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/canonical-urls)

[**404 Pages** 404s are normal, but soft 404s get pages deindexed. Return real HTTP status codes from your Vue SSR server, not client-side error components.](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/404-pages) [**Internal Linking** Build a RouterLink hub-and-spoke structure so Googlebot reaches every page instead of relying on your sitemap alone.](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/internal-linking)

**On this page**

- [Route Params vs Query Parameters](#route-params-vs-query-parameters)
- [Basic Dynamic Routes](#basic-dynamic-routes)
- [Per-Route Meta Tags](#per-route-meta-tags)
- [Multiple Route Params](#multiple-route-params)
- [Optional Params](#optional-params)
- [Catch-All Routes](#catch-all-routes)
- [Programmatic Navigation](#programmatic-navigation)
- [Common SEO Issues](#common-seo-issues)
- [Route Params Table](#route-params-table)
- [Regex Constraints](#regex-constraints)
- [Navigation Guards for Meta](#navigation-guards-for-meta)
- [TypeScript Support](#typescript-support)
- [Verification](#verification)
- [Checklist](#checklist)