feat(studio): serve SPA shell on unprefixed paths too

Keep the base-path-prefixed rewrites for assets and routes that Vite
already baked the prefix into, but also emit root-level rewrites so
bare-domain hits (including /) resolve to the shell instead of Vercel's
default 404.
This commit is contained in:
Alaister Young committed 2026-04-22 01:58:52 +08:00
1 parent 47f11bcd49
commit 9ee354ee65
2 files changed
+26 -24

No files matched your search

@@ -1,6 +1,5 @@
// api/server.ts
// @ts-ignore - built at `vite build` time, not present in source
import handler from '../dist/server/server.js'
// eslint-disable-next-line import/no-anonymous-default-export, no-restricted-exports
export default (request: Request) => handler.fetch(request)
export default (request) => handler.fetch(request)
+25 -22
View File
@@ -11,28 +11,31 @@ export const config: VercelConfig = {
outputDirectory: 'dist/client',
cleanUrls: true,
rewrites: [
// API + server-function passthrough. Both funnel through the single
// `/api/server` Vercel Function that TanStack Start generates. These
// must come before the asset rule so that extensioned API paths like
// `/api/foo.json` still hit the function instead of a filesystem lookup.
// The destination is un-prefixed because the function is registered at
// `/api/server` regardless of the app's base path.
routes.rewrite(`${basePath}/api/(.*)`, '/api/server'),
routes.rewrite(`${basePath}/_serverFn/(.*)`, '/api/server'),
// Asset passthrough. Matches anything ending in `.ext` (e.g.
// `/foo/assets/bundle.abc.js`) and rewrites to the un-prefixed path so
// Vercel finds it in `dist/client/`. If the file doesn't exist Vercel
// 404s — important, otherwise missing JS would fall through to the
// shell rule below and the browser would get HTML where JS was
// expected.
routes.rewrite(`${basePath}/(.*\\.\\w+)`, '/$1'),
// SPA fallback. Every remaining path (including the base path itself)
// gets served the prerendered shell, which boots the client router. The
// destination is `/_shell` (no extension) because `cleanUrls: true`
// makes Vercel key static assets by their clean URL — `_shell.html`
// lives at `/_shell` in the asset map, so `/_shell.html` wouldn't
// resolve.
routes.rewrite(`${basePath}/(.*)`, '/_shell'),
// Prefixed rules — only emitted when a base path is configured. Vite
// bakes `${basePath}/assets/*` URLs into the HTML, so anything hitting
// a prefixed path needs to be stripped back to its filesystem location
// (or routed to the /api/server function).
...(basePath
? [
routes.rewrite(`${basePath}/api/(.*)`, '/api/server'),
routes.rewrite(`${basePath}/_serverFn/(.*)`, '/api/server'),
routes.rewrite(`${basePath}/(.*\\.\\w+)`, '/$1'),
routes.rewrite(`${basePath}/(.*)`, '/_shell'),
]
: []),
// Root-level rules — always emitted. These serve the app at `/` and
// also cover any paths that don't carry the base-path prefix (handy for
// health checks, bare-domain hits, or links that forget the prefix).
//
// Order matters: API + server-function passthrough first so extensioned
// API paths (`/api/foo.json`) don't get caught by the asset rule. Asset
// rule next — it's an identity rewrite that also guards missing files
// from falling through to the shell (a missing `.js` should 404, not
// serve HTML). Shell rule last, catching everything else.
routes.rewrite('/api/(.*)', '/api/server'),
routes.rewrite('/_serverFn/(.*)', '/api/server'),
routes.rewrite('/(.*\\.\\w+)', '/$1'),
routes.rewrite('/(.*)', '/_shell'),
],
}