Summary
The dev/prod server rewrites /assets/* to /resources/assets/* (and back) when resolving a static request. The static build does no such thing — it copies publicDir and nothing else. A URL that resolves correctly under stx dev therefore has no counterpart in dist/, and the only way to notice is to diff the build output against the served tree.
Impact (real app)
analyticshq's marketing layout loads a script from the Stacks/Laravel-style asset root:
resources/layouts/marketing.stx:63
<script src="/assets/scripts/site-mode.js" defer></script>The file lives at resources/assets/scripts/site-mode.js, which is exactly where buddy make:* and the Stacks conventions put it, and the serve path finds it. In the static build:
$ find dist/assets -maxdepth 1 -type d
dist/assets
dist/assets/images # mirrored from public/assets/images
dist/assets/fonts # mirrored from public/assets/fonts
$ ls dist/assets/scripts
ls: dist/assets/scripts: No such file or directoryThat script drives the coming-soon gate (it reads body.dataset.siteMode), so on a static deploy the gate silently stops working on all 27 marketing pages. Nothing in the build reports it.
Evidence in stx
Serve path — packages/bun-plugin/src/serve.ts
:798 function assetRequestPaths(pathname: string): string[] {
:800 pathname,
:801 pathname.replace(/^\/assets\//, '/resources/assets/'),
:802 pathname.replace(/^\/resources\/assets\//, '/assets/'),and :3111-3112 — if (path.startsWith('/assets/') || path.startsWith('/resources/assets/')) routes through it. resources/assets is also watched for HMR at :1083.
Build path — packages/stx/src/ssg.ts
:1286 if (cfg.publicDir && fs.existsSync(cfg.publicDir)) {
:1287 await copyDirectory(cfg.publicDir, cfg.outputDir)
:1288 console.log(`Copied public assets from ${cfg.publicDir}`)grep -n 'resources/assets' packages/stx/src/ssg.ts → no matches. The build has no concept of the asset root the serve path invented.
Prior art
Vite has exactly two rules and both halves of the pipeline obey them: public/ is copied verbatim, and anything referenced from source is a hashed build asset. The same module graph feeds vite dev and vite build, so a URL cannot resolve in one and not the other. Laravel Vite compiles resources/ into public/build with a manifest that both the dev server and the built app read.
Ask
- Have the SSG copy
resources/assets/intodist/assets/(mirroring the serve-time rewrite) so the two paths agree. - Better: make
/assets/*a single documented resolution rule shared byserve.tsandssg.ts, rather than a rewrite that exists only on one side. - Emit a build warning for any
src=/href=in the generated HTML that points at a same-origin path with no file in the output directory. This class of bug is invisible until deploy.