diff --git a/.changeset/fix-ssr-bare-basepath-redirect.md b/.changeset/fix-ssr-bare-basepath-redirect.md new file mode 100644 index 00000000000..848f68cf221 --- /dev/null +++ b/.changeset/fix-ssr-bare-basepath-redirect.md @@ -0,0 +1,7 @@ +--- +'@tanstack/router-core': patch +--- + +fix(router): apply trailingSlash config to the rewritten basepath URL before the SSR redirect comparison. + +Fixes [#7291](https://github.com/TanStack/router/issues/7291): visiting a bare basepath URL (e.g. `/preview`) on the server always triggered a spurious `308` redirect to `/preview/` even when `trailingSlash` was `'never'` (the default), because the rewritten public href was synthesised from `URL` parsing — which always normalises to a trailing slash — and never reconciled with the user's `trailingSlash` config before being compared to the incoming request URL. diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index bd8a2898ec4..895e8504f2a 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -2020,8 +2020,24 @@ export class RouterCore< publicHref = rewrittenUrl.href external = true } else { + // Apply trailingSlash config to the rewritten pathname so that the + // publicHref is canonical before it is compared to the incoming URL + // in the SSR redirect check. Without this, visiting the bare basepath + // (e.g. /preview) always produces publicHref = /preview/ from the + // rewrite output and triggers a spurious 308 redirect even when + // trailingSlash is 'never'. See: https://github.com/TanStack/router/issues/7291 + const trailingSlashOpt = this.options.trailingSlash ?? 'never' + let rewrittenPathname = rewrittenUrl.pathname + if (trailingSlashOpt === 'never') { + rewrittenPathname = trimPathRight(rewrittenPathname) + } else if ( + trailingSlashOpt === 'always' && + !rewrittenPathname.endsWith('/') + ) { + rewrittenPathname += '/' + } publicHref = - rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash + rewrittenPathname + rewrittenUrl.search + rewrittenUrl.hash } } else { // Fast path: no rewrite, skip URL construction entirely