---
title: "SEO-Friendly URLs in Nuxt"
description: "Choose readable slugs, consistent casing, and stable query parameters, then check canonical URLs in Nuxt."
canonical_url: "https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering/url-structure"
last_updated: "2026-10-05"
---

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

::key-takeaways
- Use hyphens (not underscores), consistent casing, and concise readable slugs
- Both paths and query parameters can identify indexable content
- Canonicalize equivalent URLs; keep distinct pagination pages self-canonical
::

URLs appear in search results before users click. `/blog/vue-seo-guide` tells users what to expect; `/p?id=847` doesn't. Search engines also use URLs to understand page hierarchy and relevance.

Nuxt's file-based routing generates SEO-friendly URLs automatically from your `pages/` directory structure.

## Quick Setup

File-based routing in `pages/`:

```
pages/
  blog/
    [slug].vue          → /blog/vue-seo-guide
  products/
    [category]/
      [slug].vue        → /products/phones/iphone-15
```

```vue [pages/blog/[slug\\].vue]
<script setup lang="ts">
const route = useRoute()
const slug = route.params.slug

// Set canonical URL
useHead({
  link: [{
    rel: 'canonical',
    href: `https://mysite.com/blog/${slug}`
  }]
})
</script>
```

## URL Formatting Rules

### Hyphens Over Underscores

[Google treats hyphens as word separators](https://developers.google.com/search/docs/crawling-indexing/url-structure). Underscores connect words into single terms.

```
✅ /performance-optimization    → "performance" + "optimization"
❌ /performance_optimization    → "performanceoptimization"
```

**Nuxt file structure:**

```
pages/
  learn-vue-router.vue     ✅ Good
  learn_vue_router.vue     ❌ Bad
```

### Lowercase Only

URLs are case-sensitive. `/About`, `/about`, and `/ABOUT` are different pages. This creates [duplicate content](https://nuxtseo.com/learn-seo/nuxt/controlling-crawlers/duplicate-content) issues.

```
✅ /about
✅ /products/phones
❌ /About
❌ /products/Phones
```

Use [`kebabCase` from scule](https://github.com/unjs/scule) to generate lowercase slugs automatically.

### Keep URLs Short

Keep URLs concise enough to read and share. Google does not document a ranking advantage for a 60-character cutoff.

**When longer URLs make sense:**

```
✅ /docs/getting-started/installation-guide    (clear hierarchy)
✅ /blog/2025/fixing-vue-hydration-mismatch   (date + topic)
❌ /the-ultimate-comprehensive-complete-guide  (keyword stuffing)
```

### Keywords Near the Start

Google's own guidance is that a [URL should describe the page's content clearly](https://developers.google.com/search/docs/crawling-indexing/url-structure) so users and crawlers can identify the concept at a glance. Front-load the important terms.

```
✅ /vue-router-seo-guide
✅ /seo/vue-best-practices
❌ /guides-and-tutorials-for-seo-in-vue-router
```

But don't sacrifice readability:

```
pages/
  vue-seo/
    [topic].vue          ✅ Natural keyword placement
  vue-seo-guide-vue-router-seo-tutorial.vue  ❌ Keyword stuffing
```

### Avoid Dates (Usually)

Dates in URLs can look stale when an evergreen guide changes. They do not prevent you from updating the content.

```
❌ /blog/2024/vue-router-guide      (looks stale)
❌ /blog/2024/12/17/post-title      (dated path for evergreen content)
✅ /blog/vue-router-guide           (can be updated anytime)
```

**Exception:** Time-sensitive content like news, events, changelogs:

```
pages/
  changelog/
    [year]/
      [month]/
        [slug].vue       → /changelog/2025/12/new-feature
  events/
    2025/
      [slug].vue         → /events/2025/nuxt-conf
  blog/
    [slug].vue           → /blog/vue-router-guide (evergreen)
```

Choose the URL before publishing. If a dated URL already exists, updating the article does not require changing it.

## Path Segments vs Query Parameters

Both path segments and query parameters can identify indexable pages. Choose a stable structure that describes the content and avoids unnecessary duplicate URLs.

**Comparison:**

| Type             | Example                           | SEO Impact                            |
| ---------------- | --------------------------------- | ------------------------------------- |
| Path segments    | `/products/phones/iphone-15`      | Readable content path                 |
| Query parameters | `/products?category=phones&id=15` | ⚠️ Duplicate content risk             |
| Mixed            | `/products/phones?sort=price`     | ✅ Path for content, query for filters |

**Problems with query parameters:**

```
/products
/products?sort=price
/products?sort=date
/products?page=2
/products?sort=price&page=2
```

These URLs may represent duplicates or distinct content. Pagination and filters can change the items shown. Google [spends crawl time on the duplicates instead of new or updated pages](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls). See [Query Parameters](https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering/query-parameters) for handling strategies.

### Nuxt Dynamic Routes

Dynamic segments are one way to give content stable paths:

```
pages/
  products/
    [category]/
      [slug].vue        → /products/phones/iphone-15
  blog/
    [year]/
      [month]/
        [slug].vue      → /blog/2025/12/nuxt-seo-guide
  docs/
    [section]/
      [page].vue        → /docs/getting-started/installation
```

Query parameters can represent sorting, filtering, or pagination. Consolidate only equivalent pages; distinct paginated pages need their own canonical URLs. See the [Query Parameters guide](https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering/query-parameters) for implementation details including canonical composables and parameter ordering.

## Dynamic Routes

The data examples below cover initial page setup. Your app must provide the named endpoints and fields. Add route-change handling if a page component remains mounted for another route.

Nuxt's file-based routing creates SEO-friendly URLs automatically. Descriptive slugs help readers understand the destination; they do not guarantee higher rankings than IDs.

### Basic Dynamic Segments

```
pages/
  blog/
    [slug].vue          → /blog/:slug
```

```vue [pages/blog/[slug\\].vue]
<script setup lang="ts">
const route = useRoute()
const slug = route.params.slug

// Fetch content based on slug
const { data: post, error } = await useFetch<{ title: string, excerpt: string }>(`/api/posts/${encodeURIComponent(String(slug))}`)
if (error.value)
  throw error.value
if (!post.value)
  throw createError({ statusCode: 404, statusMessage: 'Post not found' })

// Set meta tags
useSeoMeta({
  title: () => post.value?.title,
  description: () => post.value?.excerpt,
  ogUrl: `https://mysite.com/blog/${slug}`
})

useHead({
  link: [{
    rel: 'canonical',
    href: `https://mysite.com/blog/${slug}`
  }]
})
</script>
```

### Nested Dynamic Routes

```
pages/
  products/
    [category]/
      [slug].vue        → /products/:category/:slug
```

Generates hierarchical URLs:

- `/products/electronics/laptop`
- `/products/clothing/jacket`

```vue [pages/products/[category\\]/[slug\\].vue]
<script setup lang="ts">
const route = useRoute()
const category = route.params.category
const slug = route.params.slug

const { data: product, error } = await useFetch<{ name: string, description: string }>(
  `/api/products/${encodeURIComponent(String(category))}/${encodeURIComponent(String(slug))}`
)
if (error.value)
  throw error.value
if (!product.value)
  throw createError({ statusCode: 404, statusMessage: 'Product not found' })

useSeoMeta({
  title: () => `${product.value?.name} - ${category}`,
  description: () => product.value?.description
})
</script>
```

### Catch-All Routes

```
pages/
  search/
    [...params].vue     → /search/:params*
```

Matches:

- `/search/vue-router`
- `/search/vue-router/recent`
- `/search/vue-router/recent/2025`

**Important:** canonicalize catch-all variations only when they represent equivalent content. This initial-load excerpt assumes later segments do not change the page:

```vue [pages/search/[...params\\].vue]
<script setup lang="ts">
const route = useRoute()
const query = Array.isArray(route.params.params)
  ? route.params.params[0]
  : route.params.params

useHead({
  link: [{
    rel: 'canonical',
    href: `https://mysite.com/search/${query}`
  }]
})
</script>
```

## Slug Generation

Use [`kebabCase` from scule](https://github.com/unjs/scule) (already a Nuxt dependency):

```ts
import { kebabCase } from 'scule'

kebabCase('Vue Router Guide') // "vue-router-guide"
kebabCase('50% Off Sale!') // "50-off-sale"
```

For international content, [Google supports UTF-8 URLs](https://developers.google.com/search/docs/crawling-indexing/url-structure) but they must be percent-encoded when sent over HTTP. Consider whether your audience expects localized slugs or ASCII-only.

## URL Hierarchy and Internal Linking

Use a hierarchy that helps readers navigate. [Internal links](https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering/internal-linking) provide crawl paths; path depth alone does not establish page importance.

Use internal links to make important pages easy to reach. Use hub pages (like `/features`) that link to all child pages, creating a clear topic cluster.

When generating pages at scale ([programmatic SEO](https://nuxtseo.com/learn-seo/nuxt/routes-and-rendering/programmatic-seo)), use URL patterns that include target keywords:

```
pages/
  compare/
    [slug].vue            → /compare/pocketui-vs-css-peeper
  alternatives/
    [slug].vue            → /alternatives/colorzilla
  features/
    [slug].vue            → /features/color-extraction
```

Each pattern targets a different keyword cluster. Keep slugs descriptive: `/compare/pocketui-vs-css-peeper` rather than `/compare/1`, so both users and crawlers know what the page compares before they open it.

## Testing URL Structure

**View in search results:**

```bash
# Test how Google displays your URLs
site:yoursite.com "nuxt seo"
```

**Check canonicalization:**

Use [Google Search Console URL Inspection](https://search.google.com/search-console) to verify:

- User-declared canonical matches your intent
- Google-selected canonical agrees with your preference
- No conflicting signals from redirects or alternates

## Checklist

::checklist{#nuxt-url-structure}
- URLs use hyphens, not underscores, to separate words
- All paths are lowercase
- Indexed content has stable paths or query URLs
- Canonical URLs distinguish equivalent variations from distinct content
- Consider dated paths before publishing; retain established URLs when updating
- Slugs are descriptive (`laptop-pro-15`, not `84792`)
::

## Sitemap

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