Environment
- Nitro:
2.13.4 (via Nuxt 4.4.8)
- Node:
24.18.0
- Preset:
vercel
Reproduction
nuxt.config.ts (no Nuxt modules needed):
export default defineNuxtConfig({
nitro: { prerender: { crawlLinks: false, routes: ['/', '/slash/', '/noslash'] } },
})
NITRO_PRESET=vercel npm run build
node -e "console.log(require('./.vercel/output/config.json').overrides)"
npx vercel deploy --prebuilt # then GET /slash/ and /noslash a few times
StackBlitz/WebContainers can't run the Vercel build/runtime, so use a real vercel build + deploy to observe it.
Describe the bug
A prerendered route with a trailing slash gets a Build Output overrides entry whose path keeps the slash, and Vercel does not serve the static file from such a path. The request falls through to the SSR function.
Generated overrides:
Each page renders a server-side useState(new Date().toISOString()) (frozen if static, fresh per request if the function runs):
| Route |
override path |
x-vercel-cache |
timestamp over 3 requests |
served by |
/slash/ |
slash/ |
MISS |
changes each request |
function ❌ |
/noslash |
noslash |
HIT |
frozen |
static ✓ |
Changing only slash's override from slash to slash/ on the same build flips it from static to function, so the trailing slash is the sole cause.
Root cause
src/presets/vercel/utils.ts, generateBuildConfig():
{ path: route.replace(/^\//, '') } // strips the leading slash, KEEPS the trailing slash
Prerender routes are stored verbatim, so a /slash/ route becomes { path: "slash/" }. Per the Build Output API v3 spec an override path is always slash-free (blog.html → { path: "blog" }). Trailing-slash behavior is expressed via 308 redirect routes, not the override path. So { path: "slash/" } is off-spec.
Suggested fix
Make the override path slash-free, either:
- Omit the override for a
<dir>/index.html (Vercel's directory-index already serves it at both /dir and /dir/), or
- emit a clean
path plus 308 trailing-slash redirect routes (e.g. @vercel/routing-utils getTransformedRoutes({ trailingSlash })).
Note: rewriting to { path: "slash" } alone would leave the canonical /slash/ unserved, since the preset emits no redirect routes. Likely also fixes #4242. Happy to open a PR.
Additional context
Real-world trigger: a Nuxt site using @nuxtjs/i18n + nuxt-site-config with trailingSlash: true produces { "de/index.html": { "path": "de/" } } and serves /de/ from the function.
Related:
Logs
# /slash/ (override "slash/") -> timestamp changes = function
req1 x-vercel-cache=MISS renderedAt=2026-06-28T14:20:44.124Z
req2 x-vercel-cache=MISS renderedAt=2026-06-28T14:20:44.563Z
req3 x-vercel-cache=MISS renderedAt=2026-06-28T14:20:45.019Z
# /noslash (override "noslash") -> timestamp frozen = static
req1-3 x-vercel-cache=HIT renderedAt=2026-06-28T14:20:06.367Z
Environment
2.13.4(via Nuxt4.4.8)24.18.0vercelReproduction
/slash/(broken) vs/noslash(works)nuxt.config.ts(no Nuxt modules needed):Describe the bug
A prerendered route with a trailing slash gets a Build Output
overridesentry whosepathkeeps the slash, and Vercel does not serve the static file from such a path. The request falls through to the SSR function.Generated
overrides:{ "index.html": { "path": "" }, // static ✓ "noslash/index.html": { "path": "noslash" }, // static ✓ "slash/index.html": { "path": "slash/" } // function ❌, trailing slash }Each page renders a server-side
useState(new Date().toISOString())(frozen if static, fresh per request if the function runs):pathx-vercel-cache/slash/slash/MISS/noslashnoslashHITChanging only
slash's override fromslashtoslash/on the same build flips it from static to function, so the trailing slash is the sole cause.Root cause
src/presets/vercel/utils.ts,generateBuildConfig():Prerender routes are stored verbatim, so a
/slash/route becomes{ path: "slash/" }. Per the Build Output API v3 spec an overridepathis always slash-free (blog.html → { path: "blog" }). Trailing-slash behavior is expressed via 308 redirectroutes, not the overridepath. So{ path: "slash/" }is off-spec.Suggested fix
Make the override
pathslash-free, either:<dir>/index.html(Vercel's directory-index already serves it at both/dirand/dir/), orpathplus 308 trailing-slash redirectroutes(e.g.@vercel/routing-utilsgetTransformedRoutes({ trailingSlash })).Note: rewriting to
{ path: "slash" }alone would leave the canonical/slash/unserved, since the preset emits no redirect routes. Likely also fixes #4242. Happy to open a PR.Additional context
Real-world trigger: a Nuxt site using
@nuxtjs/i18n+nuxt-site-configwithtrailingSlash: trueproduces{ "de/index.html": { "path": "de/" } }and serves/de/from the function.Related:
index.htmlnot served statically by Vercel preset — root URL goes through SSR function #4242 (open, root/variant)Logs