SEO-Friendly URLs in Vue

Create search-optimized URLs using Vue Router. Learn slug formatting, parameter handling, and route patterns that improve rankings.
Harlan WiltonHarlan Wilton8 mins read Published
What you'll learn
  • 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 use URLs to understand page hierarchy and relevance. Well-structured URLs improve click-through rates by up to 15%.

For Vue applications requiring SSR, use Unhead for meta tags and configure Vue Router with proper slug patterns.

Quick Setup

router.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
<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.

URL Formatting Rules

Hyphens Over Underscores

Google treats hyphens as word separators. Underscores connect words into single terms.

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

Google's Matt Cutts confirmed in 2011: "We use the words in a URL as a very lightweight factor... we can't easily segment at underscores."

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 issues.

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

Use kebabCase from 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:

URLLengthResult
/blog/vue-seo14 chars✅ Displays fully
/blog/comprehensive-guide-to-vue-seo-optimization50 chars⚠️ Works but verbose
/blog/a-comprehensive-guide-to-vue-server-side-rendering-seo-optimization-best-practices98 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

Including keywords in URLs provides a lightweight ranking boost. Front-load 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:

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 allows republishing old posts with new content without changing URLs. a strong SEO strategy.

Path Segments vs Query Parameters

Search engines prefer path segments over query parameters. Path segments are indexed and ranked. Query parameters often cause duplicate content.

Comparison:

TypeExampleSEO 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 sees duplicate content and wastes crawl budget.

Vue Router Path Segments

Use dynamic segments for content that should be indexed:

router.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

Query Parameters for Filters

Use query parameters for sorting, filtering, pagination. features that modify display without changing core content:

pages/Products.vue
<script setup lang="ts">
const route = useRoute()
const category = route.params.category
const sort = route.query.sort || 'popular'
const page = route.query.page || '1'

// Canonical URL excludes query params
useHead({
  link: [{
    rel: 'canonical',
    href: `https://mysite.com/products/${category}`
  }]
})
</script>

<template>
  <div>
    <!-- URL: /products/phones?sort=price&page=2 -->
    <!-- Canonical: /products/phones -->
  </div>
</template>

Set canonical URLs to consolidate ranking signals:

// Filter/sort variations point to base URL
useHead({
  link: [{
    rel: 'canonical',
    href: `https://mysite.com/products/${category}`
  }]
})

Dynamic Routes

Vue Router's dynamic routes 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
const routes = [
  {
    path: '/blog/:slug',
    component: BlogPost,
    props: true
  }
]
pages/BlogPost.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
const routes = [
  {
    path: '/products/:category/:slug',
    component: Product,
    props: true
  }
]

Generates hierarchical URLs:

  • /products/electronics/laptop
  • /products/clothing/jacket
pages/Product.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
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:

const route = useRoute()

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

Slug Generation

Use kebabCase from scule:

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 but they must be percent-encoded when sent over HTTP. Consider whether your audience expects localized slugs or ASCII-only.

Server Configuration

History Mode Requirements

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

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:

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

Check canonicalization:

Use Google Search Console URL Inspection to verify:

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

Using Nuxt?

If you're using Nuxt, file-based routing generates URLs automatically. The Nuxt SEO module handles canonical URLs, sitemaps, and route rules.

Learn more about URL structure in Nuxt →