written by claude
Summary
Under any non-root Vite base (e.g. base: "/repo/" for a GitHub Pages subpath deploy), @solidjs/start@2.0.0-alpha.* produces SSR HTML whose <script>, <link rel="modulepreload">, and <link rel="stylesheet"> tags are root-absolute (/_build/...) instead of base-prefixed (/repo/_build/...). The page returns 200 from the right URL, but every asset 404s, hydration never starts, and the app appears blank.
There's also a secondary papercut: @solidjs/vite-plugin-nitro-2 defaults Nitro's baseURL to / regardless of Vite's base, so users have to write the prefix twice — once as base in their Vite config and again as nitroV2Plugin({ baseURL: "/repo/" }) — to get Nitro to prerender and serve at the right path.
Reproduction
- Scaffold a SolidStart 2.0 project, set
base: "/sub/" in vite.config.ts.
pnpm build → node .output/server/index.mjs.
curl http://localhost:3000/sub/ → HTML returns, but every asset reference is /_build/... instead of /sub/_build/....
Where the URLs are emitted
packages/start/src/server/manifest/prod-ssr-manifest.ts:
path(id) → join("/", viteManifestEntry.file)
getAssets(id) → href: "/" + asset
json() → output: join("/", ...)
packages/start/src/server/manifest/dev-client-manifest.ts:
import(join("/", id)) (same bug in dev under a base)
packages/start-nitro-v2-vite-plugin/src/index.ts:
resolvedNitroConfig has no baseURL, so Nitro defaults to /.
The dev-ssr manifest already does the right thing (normalize(join(import.meta.env.BASE_URL, id))), which is the pattern to copy.
v1 comparison
In v1, Vinxi owned the build manifest and emitted output.path with the router base already prefixed, so StartServer could read it raw and it Just Worked. The v2 migration off Vinxi dropped that wiring. The fix isn't new API — it's restoring base-awareness in the two spots that lost it.
Proposed fix
PR linked below. Three small commits:
- Failing unit tests for
prod-ssr-manifest.
- Replace
"/" with import.meta.env.BASE_URL in the prod SSR manifest and the dev client manifest (using pathe.join, which collapses to /foo when base is /).
- Default
nitroConfig.baseURL to the resolved Vite config.base in the nitro-2 plugin. User-supplied baseURL still wins.
Out of scope (follow-up)
<Router> in the user's app.tsx still requires manual base={import.meta.env.BASE_URL.replace(/\/$/, "")}. Auto-passing it from StartClient/StartServer would close the loop but touches the router integration and is worth a separate discussion.
Summary
Under any non-root Vite
base(e.g.base: "/repo/"for a GitHub Pages subpath deploy),@solidjs/start@2.0.0-alpha.*produces SSR HTML whose<script>,<link rel="modulepreload">, and<link rel="stylesheet">tags are root-absolute (/_build/...) instead of base-prefixed (/repo/_build/...). The page returns 200 from the right URL, but every asset 404s, hydration never starts, and the app appears blank.There's also a secondary papercut:
@solidjs/vite-plugin-nitro-2defaults Nitro'sbaseURLto/regardless of Vite'sbase, so users have to write the prefix twice — once asbasein their Vite config and again asnitroV2Plugin({ baseURL: "/repo/" })— to get Nitro to prerender and serve at the right path.Reproduction
base: "/sub/"invite.config.ts.pnpm build→node .output/server/index.mjs.curl http://localhost:3000/sub/→ HTML returns, but every asset reference is/_build/...instead of/sub/_build/....Where the URLs are emitted
packages/start/src/server/manifest/prod-ssr-manifest.ts:path(id)→join("/", viteManifestEntry.file)getAssets(id)→href: "/" + assetjson()→output: join("/", ...)packages/start/src/server/manifest/dev-client-manifest.ts:import(join("/", id))(same bug in dev under a base)packages/start-nitro-v2-vite-plugin/src/index.ts:resolvedNitroConfighas nobaseURL, so Nitro defaults to/.The dev-ssr manifest already does the right thing (
normalize(join(import.meta.env.BASE_URL, id))), which is the pattern to copy.v1 comparison
In v1, Vinxi owned the build manifest and emitted
output.pathwith the router base already prefixed, soStartServercould read it raw and it Just Worked. The v2 migration off Vinxi dropped that wiring. The fix isn't new API — it's restoring base-awareness in the two spots that lost it.Proposed fix
PR linked below. Three small commits:
prod-ssr-manifest."/"withimport.meta.env.BASE_URLin the prod SSR manifest and the dev client manifest (usingpathe.join, which collapses to/foowhen base is/).nitroConfig.baseURLto the resolved Viteconfig.basein the nitro-2 plugin. User-suppliedbaseURLstill wins.Out of scope (follow-up)
<Router>in the user'sapp.tsxstill requires manualbase={import.meta.env.BASE_URL.replace(/\/$/, "")}. Auto-passing it fromStartClient/StartServerwould close the loop but touches the router integration and is worth a separate discussion.