Skip to content

feat: expose trailingSlash to adapters and use a single Vercel route when every route agrees - #17522

Draft
elliott-with-the-longest-name-on-github wants to merge 1 commit into
mainfrom
elliott/vercel-uniform-trailing-slash
Draft

elliott-with-the-longest-name-on-github wants to merge 1 commit into
mainfrom
elliott/vercel-uniform-trailing-slash

Conversation

@elliott-with-the-longest-name-on-github

@elliott-with-the-longest-name-on-github elliott-with-the-longest-name-on-github commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Vercel has no server at runtime for prerendered pages, so adapter-vercel handles trailingSlash for them in the routing table. Currently it adds two routes per prerendered page: one that serves the page and one that redirects the wrong trailing slash to the right one. Vercel only allows 2048 routes per deployment, so a blog with a thousand posts fails to deploy, even though every post comes from the same /blog/[slug] route.

Basically, the adapter can't do better than per-page routes without knowing which trailing slash each route wants. It can't tell whether /blog/ should redirect everything, or whether /blog/about-me has its own route with a different setting. This PR gives adapters that information. When every route agrees, the adapter replaces all of the per-page routes with one global rule. That's the case for most apps, since 'never' is the default.

What changed

  • @sveltejs/kit: builder.routes now includes each route's trailingSlash. It's resolved the same way as at runtime: the page's option, then the endpoint's, then 'never'.
  • @sveltejs/adapter-vercel / @sveltejs/adapter-static (the copy it uses when building on Vercel):
    • Every route uses 'never': a single 308 from ^/(.+)/$ to /$1, placed before the filesystem phase.
    • Every route uses 'always': this is harder, because Vercel ignores the trailing slash when it looks up a static file, and it also serves any foo/index.html at foo. So /foo would find the page before any redirect after the filesystem phase could run.
      • Prerendered pages are renamed to foo/__index.html, which Vercel doesn't alias to foo.
      • A miss route rewrites /foo/ to /foo/__index.html. Vercel reverts miss rewrites that don't find a file, so other requests are unaffected.
      • Anything left that doesn't end in a slash gets redirected after the filesystem phase. By that point static assets have already been served, so they aren't affected. /_app/*, __data.json and __route.js requests are excluded.
    • With paths.base, there's also a redirect from the base to base/, since Kit always gives the root a trailing slash in that case.
    • Anything else (mixed options, 'ignore', or an older Kit) keeps the existing per-page routes. fix: combine prerendered trailing slash redirects into as few Vercel routes as possible #17523 makes that fallback much smaller.
  • Static ISR routes are now inserted before the first handle marker rather than before handle: filesystem, since a miss section can now come first.

Why only when every route agrees

I considered emitting a regex per route, ordered by Kit's route priority, with exceptions for conflicting routes. That falls apart quickly:

  • reroute means a route's pattern doesn't tell you which URLs it serves. You can end up with the edge removing a slash and Kit adding it back, forever.
  • Param matchers aren't regexes, so they can't be expressed in the routing table.

If every route agrees, none of that matters, because whichever route ends up handling a URL wants the same thing.

A few behaviour changes worth knowing about:

  • Server-rendered requests with the wrong slash are now redirected at the edge instead of in the function. The result is the same, and it skips a function call.
  • URLs that don't match any route now get a 308 before their 404.
  • In 'always' apps, a +server.js endpoint that doesn't export trailingSlash defaults to 'never'. That means the app isn't uniform, so it uses the fallback.

Tests

  • split (which has no trailingSlash options, and has a reroute hook) now has prerendered pages, plus build and platform tests for the 'never' rule.
  • A new trailing-slash app uses 'always' everywhere. It covers prerendered and server-rendered pages, endpoints, static assets with and without extensions, the query string and client-side navigation (which exercises __data.json).
  • basic mixes options and keeps covering the fallback.
  • I ran all three apps' platform tests against real deployments.

CI setup needed before merging: the new app needs a Vercel project with its root directory set to packages/adapter-vercel/test/apps/trailing-slash, and its ID stored as the VERCEL_PROJECT_ID_TRAILING_SLASH secret. The workflows already reference it.

With paths.base set, the 'always' rename needs #17525, which fixes the overrides keys for apps that use a base path. Once that's merged, I'll rebase this and add the base to the rename as well.

@vercel

vercel Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
kit-ts-always Error Error Oct 22, 2026 6:18pm UTC
kit-ts-split Error Error Oct 22, 2026 6:18pm UTC

Request Review

@pkg-svelte-dev

Copy link
Copy Markdown

Install the latest version of @sveltejs/kit from 2d0cb08:

pnpm add https://pkg.svelte.dev/@sveltejs/kit/c/2d0cb08c7a222220c842921e0aaf003f7f412a72

Open in pkg.svelte.dev: https://pkg.svelte.dev/repos/kit/pr/17522

@elliott-with-the-longest-name-on-github
elliott-with-the-longest-name-on-github added this pull request to stack #17524 October 22, 2026 18:17
@changeset-bot

changeset-bot Bot commented Oct 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2d0cb08

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@sveltejs/kit Minor
@sveltejs/adapter-vercel Patch
@sveltejs/adapter-static Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@elliott-with-the-longest-name-on-github elliott-with-the-longest-name-on-github changed the title fix: normalize trailing slashes with a single route when every route agrees feat: expose trailingSlash to adapters and use a single Vercel route when every route agrees Oct 22, 2026
@svelte-docs-bot

Copy link
Copy Markdown

@elliott-with-the-longest-name-on-github
elliott-with-the-longest-name-on-github deployed to @sveltejs/adapter-vercel platform tests October 22, 2026 22:36 — with GitHub Actions Active
@elliott-with-the-longest-name-on-github
elliott-with-the-longest-name-on-github deployed to @sveltejs/adapter-vercel platform tests October 22, 2026 22:36 — with GitHub Actions Active
@elliott-with-the-longest-name-on-github
elliott-with-the-longest-name-on-github deployed to @sveltejs/adapter-vercel platform tests October 22, 2026 22:37 — with GitHub Actions Active

This branch was successfully deployed

1 active deployment
@sveltejs/adapter-vercel platform tests — 2d0cb08c Deployed Oct 22, 2026 by elliott-with-the-longest-name-on-github via vercel / split / deploy #1536
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant