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

Nuxt SEO on GitHub

# **Dynamic Routes in Nuxt for SEO**

Set per-route meta tags on Nuxt's file-based dynamic routes, and stop query parameters from creating duplicate content in Google's index.

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

**What you'll learn**

- Route params create semantic URLs like `**/blog/:slug**`; query strings pile up into separate crawlable variations
- Fetch data with `**useFetch()**` or `**useAsyncData()**` before setting meta tags, so search engines see complete HTML instead of a loading state
- Canonical URLs should strip tracking params like `**?ref=**` and `**?utm_source=**` to stop duplicate content
- Invalid route params should return a real 404, not a rendered page with empty content

Dynamic routes generate clean URLs from parameters: `**/blog/vue-seo-guide**` instead of `**/blog?id=123**`. Search engines prefer semantic paths over 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.

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

Create files with `**[param]**` syntax in the `**pages/**` directory:

```
pages/
├── blog/
│   └── [slug].vue          → /blog/:slug
├── products/
│   └── [category]/
│       └── [id].vue        → /products/:category/:id
```

This generates:

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

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

pages/blog/\[slug].vue

```vue
<script setup lang="ts">
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. Fetch data and set meta tags with Nuxt's data fetching composables.

### Dynamic Meta from Data

Fetch data and set specific meta tags per page:

pages/blog/\[slug].vue

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

// Nuxt handles SSR automatically
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`)

// Or with useAsyncData
const { data: post } = await useAsyncData(`post-${route.params.slug}`, () =>
  fetchPost(route.params.slug))

// Set SEO meta tags
useSeoMeta({
  title: post.value?.title,
  description: post.value?.excerpt,
  ogTitle: post.value?.title,
  ogDescription: post.value?.excerpt,
  ogImage: post.value?.coverImage
})
</script>

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

Nuxt's `**useFetch()**` and `**useAsyncData()**` work automatically during SSR, so search engines see complete HTML with correct meta tags and no additional setup required.

### Using Nuxt Content

For content-driven sites, integrate with `**@nuxt/content**`:

pages/blog/\[slug].vue

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

const { data: post } = await useAsyncData(`post-${route.params.slug}`, () =>
  queryCollection('blog').where('slug', '=', route.params.slug).first())

if (!post.value) {
  throw createError({
    statusCode: 404,
    statusMessage: 'Post not found'
  })
}

useSeoMeta({
  title: post.value.title,
  description: post.value.description
})
</script>

<template>
  <ContentRenderer :value="post" />
</template>
```

## Multiple Route Params

Combine params with nested directories:

```
pages/
└── docs/
    └── [category]/
        └── [page].vue      → /docs/:category/:page
```

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

Access all params:

pages/docs/\[category]/\[page].vue

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

// Fetch content based on both params
const { data: doc } = await useFetch(`/api/docs/${category}/${page}`)

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

## Optional Params

Nuxt supports optional params with multiple page files:

```
pages/
└── blog/
    ├── [slug].vue          → /blog/:slug
    └── [category]/
        └── [slug].vue      → /blog/:category/:slug
```

Matches both:

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

Handle optional params in components:

```vue
<script setup lang="ts">
const route = useRoute()
const category = route.params.category || 'general'
</script>
```

## Catch-All Routes

Use `**[...slug].vue**` for catch-all patterns:

```
pages/
└── [...slug].vue           → Matches any path
```

Or nested catch-all:

```
pages/
└── files/
    └── [...path].vue       → /files/* matches all nested paths
```

Access the full path:

pages/files/\[...path].vue

```vue
<script setup lang="ts">
const route = useRoute()
const path = route.params.path // Array: ['docs', 'api.md']
const fullPath = Array.isArray(path) ? path.join('/') : path
</script>
```

For 404 handling, create a catch-all:

pages/\[...slug].vue

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

<template>
  <div>
    <h1>404 - Page Not Found</h1>
  </div>
</template>
```

Or use Nuxt's dedicated error page:

error.vue

```vue
<script setup lang="ts">
const error = useError()

useHead({
  title: `${error.statusCode} - ${error.statusMessage}`,
  meta: [
    { name: 'robots', content: 'noindex, nofollow' }
  ]
})
</script>

<template>
  <div>
    <h1>{{ error.statusCode }}: {{ error.statusMessage }}</h1>
  </div>
</template>
```

## Programmatic Navigation

Navigate to dynamic routes with `**navigateTo()**`:

```vue
<script setup lang="ts">
function viewPost(slug: string) {
  // Updates URL and triggers meta updates
  navigateTo(`/blog/${slug}`)
}

function viewProduct(category: string, id: number) {
  navigateTo(`/products/${category}/${id}`)
}
</script>
```

Or use `**useRouter()**`:

```vue
<script setup lang="ts">
const router = useRouter()

function viewPost(slug: string) {
  navigateTo(`/blog/${slug}`)
}
</script>
```

## 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 use Nuxt SEO Utils which handles this automatically:

[SEO Utils  **  ** SEO utilities to improve your Nuxt sites discoverability and shareability.](https://nuxtseo.com/docs/seo-utils/getting-started/introduction)

### 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 { data: post } = await useFetch(`/api/posts/${route.params.slug}`)

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

### Issue 3: 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 with route middleware:

middleware/filter-noindex.global.ts

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

Or block in `**robots.txt**` with the Robots module:

[Robots  **  ** Tame the robots crawling and indexing your site with ease.](https://nuxtseo.com/docs/robots/getting-started/introduction)

nuxt.config.ts

```ts
export default defineNuxtConfig({
  robots: {
    disallow: [
      '/*?ref=*',
      '/*?utm_*'
    ]
  }
})
```

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

### Issue 4: 404 Handling

Return proper 404 status codes for missing content:

```vue
<script setup lang="ts">
const route = useRoute()
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`)

if (!post.value) {
  throw createError({
    statusCode: 404,
    statusMessage: 'Post not found'
  })
}
</script>
```

Nuxt automatically sets the response status code to 404 during SSR, preventing Google from indexing non-existent pages. If you're seeing unexpected 404s in Search Console, see the [**~~indexing issues guide~~**](https://nuxtseo.com/learn-seo/nuxt/launch-and-listen/indexing-issues) for diagnosis steps.

## Route Params Table

Common dynamic route patterns:

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

## Route Validation

Validate params with `**definePageMeta()**`:

pages/user/\[id].vue

```vue
<script setup lang="ts">
definePageMeta({
  validate: async (route) => {
    // Only allow numeric IDs
    return /^\d+$/.test(route.params.id as string)
  }
})
</script>
```

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

## TypeScript Support

Type route params:

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

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

Or use Nuxt's generated types:

```vue
<script setup lang="ts">
// Nuxt auto-generates route types
const route = useRoute('blog-slug') // Type-safe params based on file structure
</script>
```

## 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".

Nuxt's SSR works by default, so you should always see complete HTML in the source.

## Checklist

**Checklist**

- Dynamic routes use `**[param]**` file syntax instead of query strings for canonical content
- Meta tags are set from fetched data with `**useSeoMeta()**` or `**useHead()**`, not left generic
- Canonical URLs strip tracking and filter query params
- Missing content throws `**createError({ statusCode: 404 })**` instead of rendering an empty page
- Filtered or sorted URL variants get `**noindex**` or a `**robots.txt**` disallow rule
- View Source and Search Console's URL Inspection both show the real title, not a loading state

[**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/nuxt/routes-and-rendering/url-structure)

[**Programmatic SEO**](https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering/programmatic-seo)

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

[**Indexing Issues**](https://nuxtseo.com/learn-seo/nuxt/launch-and-listen/indexing-issues)

[**404 Pages** 404s are normal, but soft 404s get pages deindexed. Return real HTTP status codes in Nuxt with error.vue and createError().](https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering/404-pages) [**Internal Linking** Build a NuxtLink hub-and-spoke structure so Googlebot reaches every page instead of leaving routes orphaned in your sitemap.](https://nuxtseo.com/learn-seo/nuxt/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)
- [Route Validation](#route-validation)
- [TypeScript Support](#typescript-support)
- [Verification](#verification)
- [Checklist](#checklist)