Index of every page on this site: llms.txt

SEO-Friendly URLs in Nuxt

Choose readable slugs, consistent casing, and stable query parameters, then check canonical URLs in Nuxt.
Harlan Wilton8 mins read Published Updated
What you'll learn
  • 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
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. 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 issues.

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

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

TypeExampleSEO Impact
Path segments/products/phones/iphone-15Readable 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. See 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 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
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
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:

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 (already a Nuxt dependency):

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.

URL Hierarchy and Internal Linking

Use a hierarchy that helps readers navigate. Internal links 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), 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:

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

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

Checklist

Checklist
  • 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)
Was this page helpful?