---
title: "SEO-Friendly URLs in Vue · Nuxt SEO"
canonical_url: "https://nuxtseo.com/learn-seo/vue/routes-and-rendering/url-structure"
last_updated: "2026-07-16T12:00:00.000Z"
meta:
  author: "Harlan Wilton"
  description: "Vue Router gives you no free URLs. Get slugs, casing, and query params right by hand before duplicates hurt your rankings."
  "og:description": "Vue Router gives you no free URLs. Get slugs, casing, and query params right by hand before duplicates hurt your rankings."
  "og:title": "SEO-Friendly URLs in Vue · Nuxt SEO"
---

Nuxt SEO on GitHub

# **SEO-Friendly URLs in Vue**

Vue Router gives you no free URLs. Get slugs, casing, and query params right by hand before duplicates hurt your rankings.

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

**What you'll learn**

- Vue Router requires manual route definitions; design your URL hierarchy intentionally
- Use hyphens as word separators (not underscores). Google treats hyphens as word breaks
- Keep URLs under 60 characters and lowercase only
- Use path segments for content, query parameters for filters and sorting

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.

Vue Router gives you full control over URL structure, but that control comes with responsibility. There is no file based routing to generate clean URLs automatically; you define every route manually in your router config. This means you choose the slug patterns, parameter handling, and hierarchy from scratch using `**createRouter()**` and `**createWebHistory()**`.

## Quick Setup

router.ts

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

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/blog/:slug',
      component: BlogPost
    },
    {
      path: '/products/:category/:slug',
      component: ProductPage
    }
  ]
})
```

pages/BlogPost.vue

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

For Vue applications, you'll need to [**~~install Unhead manually~~**](https://unhead.unjs.io/docs/vue/head/guides/get-started/installation).

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

**Vue Router implementation:**

```
// ✅ Good
{ path: '/learn-vue-router', component: Guide }

// ❌ Bad
{ path: '/learn_vue_router', component: Guide }
```

### Lowercase Only

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

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

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

### Keep URLs Short

URLs under 60 characters perform better in search results. Longer URLs get truncated with ellipsis.

**Length comparison:**

| **URL** | **Length** | **Result** |
| --- | --- | --- |
| `**/blog/vue-seo**` | 14 chars | ✅ Displays fully |
| `**/blog/comprehensive-guide-to-vue-seo-optimization**` | 50 chars | ⚠️ Works but verbose |
| `**/blog/a-comprehensive-guide-to-vue-server-side-rendering-seo-optimization-best-practices**` | 98 chars | ❌ Truncated in results |

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

```
// ✅ Natural keyword placement
{ path: '/vue-seo/:topic', component: Guide }

// ❌ Keyword stuffing
{ path: '/vue-seo-guide-vue-router-seo-tutorial', component: Guide }
```

### Avoid Dates (Usually)

Dates in URLs prevent content updates. `**/blog/2024/vue-guide**` becomes outdated when you refresh it in 2025.

```
❌ /blog/2024/vue-router-guide      (looks stale)
❌ /blog/2024/12/17/post-title      (prevents evergreen updates)
✅ /blog/vue-router-guide           (can be updated anytime)
```

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

```ts
const routes = [
  // News/events - dates make sense
  { path: '/changelog/:year/:month/:slug', component: Changelog },
  { path: '/events/2025/:slug', component: Event },

  // Evergreen content - skip dates
  { path: '/blog/:slug', component: BlogPost },
  { path: '/guides/:slug', component: Guide }
]
```

Removing dates lets you republish old posts with new content without changing the URL, so links and rankings built up over time carry forward instead of starting over.

## Path Segments vs Query Parameters

![Path vs Query Parameter Decision](https://nuxtseo.com/images/learn-seo/vue/path-vs-query-decision.svg)

Search engines prefer path segments over query parameters: [**~~static, keyword-based URLs have a higher likelihood of ranking~~**](https://www.searchenginejournal.com/technical-seo/url-parameter-handling/) than parameter-based ones. Query parameters often cause duplicate content.

**Comparison:**

| **Type** | **Example** | **SEO Impact** |
| --- | --- | --- |
| Path segments | `**/products/phones/iphone-15**` | ✅ Clean, indexed, ranks well |
| 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
```

Five URLs, same content. 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/vue/routes-and-rendering/query-parameters) for handling strategies.

### Vue Router Path Segments

Use dynamic segments for content that should be indexed:

router.ts

```ts
const routes = [
  // ✅ Path segments for SEO
  {
    path: '/products/:category/:slug',
    component: Product
  },
  {
    path: '/blog/:year/:month/:slug',
    component: BlogPost
  },
  {
    path: '/docs/:section/:page',
    component: Documentation
  }
]
```

This generates clean URLs:

- `**/products/phones/iphone-15**`
- `**/blog/2025/12/vue-seo-guide**`
- `**/docs/getting-started/installation**`

Use query parameters for sorting, filtering, and pagination instead. Set canonical URLs to consolidate these variations. See the [**~~Query Parameters guide~~**](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/query-parameters) for implementation details including canonical composables and parameter ordering.

## Dynamic Routes

[**~~Vue Router's dynamic routes~~**](https://router.vuejs.org/guide/essentials/dynamic-matching.html) create SEO-friendly URLs. Use descriptive slugs rather than database IDs: `**/products/laptop-pro-15**` ranks better than `**/products/84792**`.

### Basic Dynamic Segments

router.ts

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

pages/BlogPost.vue

```vue
<script setup lang="ts">
const props = defineProps<{ slug: string }>()

// Fetch content based on slug
const { data: post } = await useFetch(`/api/posts/${props.slug}`)

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

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

### Nested Dynamic Routes

router.ts

```ts
const routes = [
  {
    path: '/products/:category/:slug',
    component: Product,
    props: true
  }
]
```

Generates hierarchical URLs:

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

pages/Product.vue

```vue
<script setup lang="ts">
const props = defineProps<{
  category: string
  slug: string
}>()

const { data: product } = await useFetch(
  `/api/products/${props.category}/${props.slug}`
)

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

### Multiple Optional Parameters

router.ts

```ts
const routes = [
  {
    path: '/search/:query/:filters?',
    component: SearchResults
  }
]
```

Matches both:

- `**/search/vue-router**`
- `**/search/vue-router/recent**`

**Important:** Optional parameters create multiple URLs for similar content. Use canonical tags:

```ts
const route = useRoute()

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

## Slug Generation

Use [`**kebabCase**`**~~ from scule~~**](https://github.com/unjs/scule):

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

URL structure defines your site's hierarchy, and that hierarchy directly affects how [**~~internal links~~**](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/internal-linking) distribute authority. Flat structures (fewer path segments) make pages easier to crawl and link to. Deep nesting (4+ levels) signals low importance to Google.

| **Structure** | **Crawl Depth** | **SEO Impact** |
| --- | --- | --- |
| `**/features/color-extraction**` | 2 levels | Good, easy to reach |
| `**/docs/v2/api/reference/methods/extract**` | 6 levels | Poor, buried content |

Keep important pages within 3 clicks of the homepage. Use hub pages (like `**/features**`) that link to all child pages, creating a clear topic cluster.

When generating pages at scale, use URL patterns that include target keywords:

router.ts

```ts
const routes = [
  { path: '/compare/:slug', component: ComparePage },
  { path: '/alternatives/:slug', component: AlternativesPage },
  { path: '/features/:slug', component: FeaturePage },
]
```

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.

## Server Configuration

Vue Router's History mode requires server configuration. All routes must serve `**index.html**`:

```nginx
location / {
  try_files $uri $uri/ /index.html;
}
```

Without this configuration, direct URLs like `**/blog/vue-seo**` return 404 errors.

## Testing URL Structure

**View in search results:**

```bash
# Test how Google displays your URLs
site:yoursite.com "vue router"
```

**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**

- URLs use hyphens, not underscores, to separate words
- All paths are lowercase
- Indexed content lives on path segments, not query parameters
- Filters and sorting use query parameters with a self-referencing canonical
- Evergreen content skips dates in the URL
- Slugs are descriptive ( `**laptop-pro-15**`, not `**84792**`)
- The server serves `**index.html**` for every route (History mode)

File-based routing generates URLs automatically with canonical URLs, sitemaps, and route rules handled for you. [**~~Learn more about URL structure 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 **

[**Trailing Slashes**](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/trailing-slashes)

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

[**HTTP Redirects**](https://nuxtseo.com/learn-seo/vue/controlling-crawlers/redirects)

[**Query Parameters**](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/query-parameters)

[**Internal Linking**](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/internal-linking)

[**Routes & Rendering** Structure Vue Router routes and choose SSR, SSG, or SPA rendering so search engines can crawl, index, and rank every page.](https://nuxtseo.com/learn-seo/vue/routes-and-rendering) [**Pagination** Google indexes page 1 and buries the rest when canonicals point the wrong way. Fix pagination in Vue with self-referencing canonicals and crawlable links.](https://nuxtseo.com/learn-seo/vue/routes-and-rendering/pagination)

**On this page**

- [Quick Setup](#quick-setup)
- [URL Formatting Rules](#url-formatting-rules)
- [Path Segments vs Query Parameters](#path-segments-vs-query-parameters)
- [Dynamic Routes](#dynamic-routes)
- [Slug Generation](#slug-generation)
- [URL Hierarchy and Internal Linking](#url-hierarchy-and-internal-linking)
- [Server Configuration](#server-configuration)
- [Testing URL Structure](#testing-url-structure)
- [Checklist](#checklist)