Skip to content

fix: let the root route honor an explicit trailingSlash option when paths.base is set - #17392

Open
firatciftci wants to merge 4 commits into
sveltejs:mainfrom
firatciftci:fix-base-root-trailing-slash
Open

firatciftci wants to merge 4 commits into
sveltejs:mainfrom
firatciftci:fix-base-root-trailing-slash

Conversation

@firatciftci

Copy link
Copy Markdown

Closes #17391.


With paths.base set, the root route always redirects to the slash form (/app to /app/), even when the root layout exports trailingSlash = 'never'. This PR lets an explicit trailingSlash on the root apply to pages rendered on demand. Apps that don't set the option, and prerendered roots, keep the current behavior.

Why the rule exists

#9343 added the rule to fix #9341. With relative paths, the root's assets are emitted as ./_app/..., and those links only resolve from /app/. From /app, . resolves to /. A prerendered root also has to keep the slash, because it's written to index.html and static hosts serve it at /app/. #10763 and #11388 later aligned the preview server and the client router with the same rule.

The PR keeps both reasons intact and only changes the case neither applies to: a root rendered on demand, whose layout explicitly asks for no slash.

Changes

  • respond.js: the root route still defaults to 'always', but an explicitly set trailingSlash now wins, unless the root is being prerendered. PageNodes#trailing_slash takes a fallback so the root can tell "unset" apart from 'never'.
  • $app/paths/internal/server.js: a new relative_base(pathname) holds the relative-prefix rule. When the request is for the base itself (/app), it returns ./app instead of ., so links resolve to /app/.... Every other pathname gets the same . or ../.. prefix as before. resolve_paths in server_routing.js (assets, the hydration base and server-side route resolution) and the server-side resolve() in $app/paths/server.js each had their own copy of this rule. They both use the shared function now, which keeps <a href={resolve('/about')}> on the root from resolving to /about.
  • render.js: the hydration base expression now resolves base + '/' before removing the trailing slash, so ./app gives /app and not /ap. For . and ../.. the result is the same as before.
  • client.js: the root defaults to 'always', and the branch's slash overrides it the same way it does for every other route. As in fix: respect trailing slash option when navigating from the root page of the basepath #11388, the default only applies when the target is the root, so it doesn't leak into navigations away from it.
  • preview/index.js: the preview server only does its /app to /app/ redirect when a prerendered root index.html exists. Otherwise the request goes to the server, which applies the rule above.

Verified

I used the reproduction from the issue (adapter-node, base: '/app', root layout trailingSlash = 'never') built against this branch:

Request Before After
/app 308 → ./app/ 200
/app/ 200 308 → ../app
/app/about 200 200
/app/about/ 308 → ../about 308 → ../about
  • At /app, asset links render as ./app/_app/immutable/... and load with 200. The page hydrates. Navigating /app to /app/about to /app to back stays on one document load, with no console errors.
  • Without the trailingSlash export, /app still redirects to ./app/, as before.
  • With prerender = true and trailingSlash = 'never' on the root, the root is still written to index.html with ./_app/... links and new URL("./", location) as the base, the same as today.

Open question

A prerendered root that sets trailingSlash = 'never' still gets the slash, since its index.html can only be served from /app/. The client router doesn't know whether the root was prerendered, so a client-side navigation to it lands on /app, while a reload is served at /app/. I checked this in a browser with adapter-node and only the root prerendered. Going from /app/about to the root client-side shows /app. A reload redirects to /app/ and serves index.html with every asset loading, and navigating on from there works. There were no failed requests and no console errors, so the only difference is the address bar. I could add a build warning for this combination, or document that prerendered roots under a base path always keep the slash. I'd go with whichever you prefer.

Tests

This PR adds a small base-root-trailing-slash test app (port 5312), because options-2 asserts the default /basepath to /basepath/ redirect and needs to keep it. The app has base: '/basepath' and a root layout with trailingSlash = 'never', and it checks:

  • /basepath returns 200, and /basepath/?foo=bar returns a 308 to ../basepath?foo=bar
  • the root renders at /basepath with no failed requests, and resolve('') gives ./basepath/ during SSR
  • navigating /basepath to /basepath/about to /basepath keeps the slashless root, with and without JS

On main all 8 build-mode tests fail. On this branch all 8 pass in dev and build modes. The no-JS failures without the relative_base change are how I found the duplicated resolve() rule.

Locally, pnpm check, pnpm lint and pnpm test:unit (1063 passed) are clean. These suites pass in dev and build modes: the new app, options-2 (including the service worker runs), options, hash-based-routing and embed, plus options with server-side route resolution. In basics, two tests failed in the full runs and passed three times out of three when rerun alone: "using window.fetch does not cause false-positive warning" and "AbortSignal works with internal fetch optimization". One more, "captures and restores function snapshots on shallow navigations", fails under ROUTER_RESOLUTION=server on main as well.


Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • This message body should clearly illustrate what problems it solves.
  • [z] Ideally, include a test that fails without this PR but passes with it.

Tests

  • Run the tests with pnpm test and lint the project with pnpm lint and pnpm check

Changesets

  • If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpm changeset and following the prompts. Changesets that add features should be minor and those that fix bugs should be patch. Please prefix changeset messages with feat:, fix:, or chore:.

Edits

  • Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed.

@pkg-svelte-dev

pkg-svelte-dev Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Install the latest version of @sveltejs/kit from 5d3e9c2:

pnpm add https://pkg.svelte.dev/@sveltejs/kit/c/5d3e9c2f613be33a5c5e927eb0f9345f21a5b47b

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

Note

This PR is from a fork. A maintainer must approve approve each commit before it can be built and installed.

@changeset-bot

changeset-bot Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5d3e9c2

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

This PR includes changesets to release 1 package
Name Type
@sveltejs/kit 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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant