Dynamic Routes in Vue for SEO · Nuxt SEO

[NuxtSEO](https://nuxtseo.com/ "Home")

- [Modules](https://nuxtseo.com/docs/nuxt-seo/getting-started/introduction)
- [Tools](https://nuxtseo.com/tools)
- [Pro](https://nuxtseo.com/pro)
- [Learn SEO](https://nuxtseo.com/learn-seo/nuxt) [Releases](https://nuxtseo.com/releases)

[1.4K](https://github.com/harlan-zw/nuxt-seo)

[Nuxt SEO on GitHub](https://github.com/harlan-zw/nuxt-seo)

Learn SEO

Master search optimization

Nuxt

 Vue

[SEO Checklist](https://nuxtseo.com/learn-seo/checklist) [Pre-Launch Warmup](https://nuxtseo.com/learn-seo/pre-launch-warmup) [Backlinks & Authority](https://nuxtseo.com/learn-seo/backlinks)

[Mastering Meta](https://nuxtseo.com/learn-seo/vue/mastering-meta)

- [Titles](https://nuxtseo.com/learn-seo/vue/mastering-meta/titles)
- [Meta Description](https://nuxtseo.com/learn-seo/vue/mastering-meta/descriptions)
- [Social Sharing](https://nuxtseo.com/learn-seo/vue/mastering-meta/social-sharing)
- [Schema.org](https://nuxtseo.com/learn-seo/vue/mastering-meta/schema-org)
- [Migrating vue-meta](https://nuxtseo.com/learn-seo/vue/mastering-meta/migrating-vue-meta)
- [Rich Results](https://nuxtseo.com/learn-seo/vue/mastering-meta/rich-results)
- [Image Alt Text](https://nuxtseo.com/learn-seo/vue/mastering-meta/alt-text)

[ Controlling Crawlers](https://nuxtseo.com/learn-seo/vue/controlling-crawlers)

- [Robots.txt](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/robots-txt)
- [Sitemaps](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/sitemaps)
- [Robot Meta Tag](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/meta-tags)
- [Canonical Link Tag](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/canonical-urls)
- [HTTP Redirects](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/redirects)
- [Duplicate Content](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/duplicate-content)
- [llms.txt](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/llms-txt)

[ SPA SEO](https://nuxtseo.com/learn-seo/vue/spa)

- [Prerendering](https://nuxtseo.com/learn-seo/vue/spa/prerendering)
- [Dynamic Rendering](https://nuxtseo.com/learn-seo/vue/spa/dynamic-rendering)
- [Hydration & SEO](https://nuxtseo.com/learn-seo/vue/spa/hydration)

[ Routes & Rendering](https://nuxtseo.com/learn-seo/vue/routes-and-rendering)

- [URL Structure](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/url-structure)
- [Pagination](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/pagination)
- [Trailing Slashes](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/trailing-slashes)
- [Query Parameters](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/query-parameters)
- [Hreflang & i18n](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/i18n)
- [404 Pages](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/404-pages)
- [Dynamic Routes](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/dynamic-routes)
- [Internal Linking](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/internal-linking)
- [Rendering Modes](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/rendering)
- [Programmatic SEO](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/programmatic-seo)
- [Security](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/security)

[ SSR Frameworks](https://nuxtseo.com/learn-seo/vue/ssr-frameworks)

- [Nuxt vs Quasar](https://nuxtseo.com/learn-seo/vue/ssr-frameworks/nuxt-vs-quasar)
- [Custom Vite SSR](https://nuxtseo.com/learn-seo/vue/ssr-frameworks/vite-ssr)
- [VitePress SEO](https://nuxtseo.com/learn-seo/vue/ssr-frameworks/vitepress)

[ Launch & Listen](https://nuxtseo.com/learn-seo/vue/launch-and-listen)

- [Getting Indexed](https://nuxtseo.com/learn-seo/vue/launch-and-listen/going-live)
- [Google Search Console](https://nuxtseo.com/learn-seo/vue/launch-and-listen/search-console)
- [Core Web Vitals](https://nuxtseo.com/learn-seo/vue/launch-and-listen/core-web-vitals)
- [Indexing Issues](https://nuxtseo.com/learn-seo/vue/launch-and-listen/indexing-issues)
- [SEO Monitoring](https://nuxtseo.com/learn-seo/vue/launch-and-listen/seo-monitoring)
- [Site Migration](https://nuxtseo.com/learn-seo/vue/launch-and-listen/site-migration)
- [IndexNow](https://nuxtseo.com/learn-seo/vue/launch-and-listen/indexnow)
- [Debugging](https://nuxtseo.com/learn-seo/vue/launch-and-listen/debugging)
- [AI Search Optimization](https://nuxtseo.com/learn-seo/vue/launch-and-listen/ai-optimized-content)

1. [Learn SEO for Vue](https://nuxtseo.com/learn-seo)
2.
3. [Routes And Rendering](https://nuxtseo.com/learn-seo/vue/routes-and-rendering)
4.
5. [Dynamic Routes](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/dynamic-routes)

# Dynamic Routes in Vue for SEO

How to configure Vue Router dynamic route params, set per-route meta tags, and avoid duplicate content issues with URL parameters.

[![Harlan Wilton](https://avatars.githubusercontent.com/u/5326365?v=4)Harlan Wilton](https://x.com/harlan-zw)12 mins read Published Dec 17, 2025

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](#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.

From [Google's 2025 URL structure guidelines](https://developers.google.com/search/docs/crawling-indexing/url-structure): use clean paths for important content, reserve query parameters for filters that shouldn't be indexed.

## [Basic Dynamic Routes](#basic-dynamic-routes)

Define routes with `:param` syntax:

```
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`:

```
<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](#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](#static-meta-for-route-groups)

Define generic meta in route config:

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

Apply meta in a navigation guard:

```
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](#dynamic-meta-from-data)

Fetch data and set specific meta tags per page:

```
<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:

Express

Vite SSR

H3

```
// 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)
})
```

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

export async function render(url, manifest) {
  const app = createSSRApp(App)

  // Prefetch data on server
  await router.push(url)
  await router.isReady()

  const post = await fetchPost(router.currentRoute.value.params.slug)

  app.use(head)
  useHead({
    title: post.title,
    meta: [{ name: 'description', content: post.excerpt }]
  })

  const html = await renderToString(app)
  return { html }
}
```

```
// server/routes/blog/[slug].get.ts
import { defineEventHandler } from 'h3'

export default defineEventHandler(async (event) => {
  const slug = event.context.params.slug
  const post = await fetchPost(slug)

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

  return renderToString(app)
})
```

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

## [Multiple Route Params](#multiple-route-params)

Combine params for hierarchical URLs:

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

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

Access all params:

```
<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](#optional-params)

Make params optional with `?`:

```
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:

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

## [Catch-All Routes](#catch-all-routes)

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

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

Set appropriate meta for 404s:

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

## [Programmatic Navigation](#programmatic-navigation)

Navigate to dynamic routes programmatically:

```
<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:

```
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](#common-seo-issues)

### [Issue 1: Duplicate Content from Query Params](#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:

```
<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 use [Google's URL parameter handling](https://developers.google.com/search/docs/crawling-indexing/url-structure) via `robots.txt`:

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

### [Issue 2: Missing Titles on Dynamic Routes](#issue-2-missing-titles-on-dynamic-routes)

Generic titles hurt SEO:

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

Always override with specific content:

```
<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](#issue-3-client-side-rendering)

SPA routing means search engines see your loading state:

```
<!-- 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 [can render JavaScript](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics) but it's slower and less reliable.

### [Issue 4: Infinite Parameter Variations](#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`:

```
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](#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](#regex-constraints)

Validate params with regex:

```
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](#navigation-guards-for-meta)

Set global meta patterns in guards:

```
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](#typescript-support)

Type route params and meta:

```
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](#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**:

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

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

## [Using Nuxt?](#using-nuxt)

Nuxt 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)

---

[404 Pages 404 errors don't hurt SEO, but soft 404s do. Learn proper HTTP status codes, custom 404 design, and crawl budget optimization for Vue applications.](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/404-pages) [Internal Linking How to build an internal linking architecture in Vue that distributes PageRank, improves crawl depth, and helps Google discover every page.](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)
- [Using Nuxt?](#using-nuxt)

[GitHub](https://github.com/harlan-zw/nuxt-seo) [ Discord](https://discord.com/invite/275MBUBvgP)

### [NuxtSEO](https://nuxtseo.com/ "Home")

- [Getting Started](https://nuxtseo.com/docs/nuxt-seo/getting-started/introduction)
- [MCP](https://nuxtseo.com/docs/nuxt-seo/guides/mcp)

Modules

- [Robots](https://nuxtseo.com/docs/robots/getting-started/introduction)
- [Sitemap](https://nuxtseo.com/docs/sitemap/getting-started/introduction)
- [OG Image](https://nuxtseo.com/docs/og-image/getting-started/introduction)
- [Schema.org](https://nuxtseo.com/docs/schema-org/getting-started/introduction)
- [Link Checker](https://nuxtseo.com/docs/link-checker/getting-started/introduction)
- [SEO Utils](https://nuxtseo.com/docs/seo-utils/getting-started/introduction)
- [Site Config](https://nuxtseo.com/docs/site-config/getting-started/introduction)
- [Skew Protection](https://nuxtseo.com/docs/skew-protection/getting-started/introduction)
- [AI Ready](https://nuxtseo.com/docs/ai-ready/getting-started/introduction)

### [NuxtSEO Pro](https://nuxtseo.com/pro "Nuxt SEO Pro")

- [Getting Started](https://nuxtseo.com/pro)
- [Dashboard](https://nuxtseo.com/pro/dashboard)
- [Pro MCP](https://nuxtseo.com/pro/docs/getting-started/mcp-setup)

### [Learn SEO](https://nuxtseo.com/learn-seo "Learn SEO")

Nuxt

- [Mastering Meta](https://nuxtseo.com/learn-seo/nuxt/mastering-meta)
- [Controlling Crawlers](https://nuxtseo.com/learn-seo/nuxt/controlling-crawlers)
- [Launch & Listen](https://nuxtseo.com/learn-seo/nuxt/launch-and-listen)
- [Routes & Rendering](https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering)
- [Staying Secure](https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering/security)

Vue

- [Vue SEO Guide](https://nuxtseo.com/learn-seo/vue)
- [Mastering Meta](https://nuxtseo.com/learn-seo/vue/mastering-meta)
- [Controlling Crawlers](https://nuxtseo.com/learn-seo/vue/controlling-crawlers)
- [SPA SEO](https://nuxtseo.com/learn-seo/vue/spa)
- [SSR Frameworks](https://nuxtseo.com/learn-seo/vue/ssr-frameworks)
- [SEO Checklist](https://nuxtseo.com/learn-seo/checklist)
- [Pre-Launch Warmup](https://nuxtseo.com/learn-seo/pre-launch-warmup)
- [Backlinks & Authority](https://nuxtseo.com/learn-seo/backlinks)

### [Tools](https://nuxtseo.com/tools "SEO Tools")

- [Social Share Debugger](https://nuxtseo.com/tools/social-share-debugger)
- [Robots.txt Generator](https://nuxtseo.com/tools/robots-txt-generator)
- [Meta Tag Checker](https://nuxtseo.com/tools/meta-tag-checker)
- [HTML to Markdown](https://nuxtseo.com/tools/html-to-markdown)
- [XML Sitemap Validator](https://nuxtseo.com/tools/xml-sitemap-validator)
- [Schema.org Validator](https://nuxtseo.com/tools/schema-validator)
- [Keyword Idea Generator](https://nuxtseo.com/tools/keyword-generator)
- [Keyword Research](https://nuxtseo.com/tools/keyword-research)
- [SERP Analyzer](https://nuxtseo.com/tools/serp-analyzer)
- [Domain Rankings](https://nuxtseo.com/tools/domain-rankings)

Copyright © 2023-2026 Harlan Wilton - [MIT License](https://github.com/harlan-zw/nuxt-seo/blob/main/license) · [mdream](https://mdream.dev)