experimental.defaults.nuxtLink.trailingSlashsite.trailingSlash: trueA trailing slash is the / at the end of a URL. /about/ has one, /about doesn't. Both can serve identical content, and that's the problem.
export default defineNuxtConfig({
site: {
trailingSlash: true // handles everything if using Nuxt SEO
}
})
Most Nuxt sites don't need trailing slashes. But there are cases where you'll want them:
Static file servers treat trailing slashes as directories. Apache, Nginx, and S3 serve /about/ as /about/index.html. Without the trailing slash, some servers return 404s or require extra config.
Legacy URL structures from WordPress, Rails, or older CMSs often used trailing slashes. If you're migrating, matching the existing format avoids mass redirects.
CMS conventions in tools like Contentful, Sanity, or Storyblok may generate paths with trailing slashes. Matching their format keeps URLs predictable.
Team preference sometimes dictates format. Trailing slashes visually indicate "this is a section" to some developers.
If none of these apply, stick with Nuxt's default: no trailing slashes.
NuxtLink has built-in trailing slash support. Set it globally or per-link.
Apply trailing slashes to all internal links:
export default defineNuxtConfig({
experimental: {
defaults: {
nuxtLink: {
trailingSlash: 'append' // or 'remove'
}
}
}
})
Every <NuxtLink to="/about"> now renders as /about/.
Override the global setting on specific links:
<template>
<!-- Forces trailing slash regardless of global config -->
<NuxtLink to="/api/docs" trailing-slash="append">
API Docs
</NuxtLink>
<!-- Removes trailing slash regardless of global config -->
<NuxtLink to="/blog/" trailing-slash="remove">
Blog
</NuxtLink>
</template>
Use this for external integrations or API routes that require a specific format.
Trailing slashes become an SEO problem when both /about and /about/ exist. Search engines see two pages with identical content, splitting your ranking signals.
You need three things:
Without Nuxt SEO, configure each piece separately:
export default defineNuxtConfig({
// 1. NuxtLink trailing slashes
experimental: {
defaults: {
nuxtLink: {
trailingSlash: 'append'
}
}
},
// 2. Redirects for wrong format
routeRules: {
// Redirect /about to /about/
'/about': { redirect: '/about/' },
'/blog': { redirect: '/blog/' }
// ... every route
}
})
Then set canonicals manually on each page:
<script setup lang="ts">
const route = useRoute()
const canonicalUrl = `https://example.com${route.path}${route.path.endsWith('/') ? '' : '/'}`
useHead({
link: [{ rel: 'canonical', href: canonicalUrl }]
})
</script>
This works but doesn't scale.
Nuxt SEO handles all of it with one config:
export default defineNuxtConfig({
modules: ['@nuxtjs/seo'],
site: {
url: 'https://example.com',
trailingSlash: true
}
})
This single option:
Even with correct internal links, external sites and old bookmarks may use the wrong format. Set up server-side redirects.
export default defineNuxtConfig({
routeRules: {
// If using trailing slashes, redirect non-trailing to trailing
'/about': { redirect: { to: '/about/', statusCode: 301 } },
'/blog': { redirect: { to: '/blog/', statusCode: 301 } }
}
})
For dynamic redirects across all routes:
export default defineEventHandler((event) => {
const path = event.path
// Skip API routes and files with extensions
if (path.startsWith('/api') || path.includes('.'))
return
// Redirect non-trailing to trailing
if (!path.endsWith('/')) {
return sendRedirect(event, `${path}/`, 301)
}
})
When prerendering, Nuxt generates /about as /about/index.html by default. Static hosts like Cloudflare Pages then redirect /about → /about/ with a 308.
If you want trailing slashes, this is correct behavior.
If you don't want trailing slashes and are seeing unwanted 308 redirects, set autoSubfolderIndex: false to generate /about.html instead:
export default defineNuxtConfig({
nitro: {
prerender: {
autoSubfolderIndex: false
}
}
})