app/assets/ is the first, app/static/ is the second. Mixing them up is the easiest way to end up serving a raw font file at the wrong URL, or shipping CSS that never made it into a page.
Build inputs
app/assets/ holds stylesheets, fonts, and images, the raw material esbuild compiles and content-hashes into dist/ at build time. Nothing in this directory is served raw, and nothing here is a page template either. It’s compiler input, full stop.
A freshly scaffolded app ships one file here, app/assets/app.css, imported explicitly rather than picked up by convention:
app/assets/app.css
Importing CSS from a layout
A layout pulls in its stylesheet the same way any Svelte component imports anything, a plainimport in its <script> block:
app/templates/_layout.svelte
_layout.svelte only needs to import what it adds beyond the root, since the root layout’s CSS already reaches every page under it.
Because a route’s own bundle also reaches its layout component (that’s how hydration finds it), the same CSS can end up in both the layout’s stylesheet and the route’s. That’s harmless: CSS rules are idempotent and both files are cached immutably, so nothing renders wrong and there’s nothing to fix.
_global.css is gone
Older Fymo projects had a magic filename,app/templates/_global.css, picked up automatically and linked into every page. That auto-injection is deleted, not deprecated. A project still shipping the file fails the build with the exact fix:
.css file under app/templates/, at any depth, fails the same way, just with a more general message:
<style> blocks inside a .svelte file aren’t affected. That’s Svelte’s own component styling, and this check has no opinion about it.
Fonts
A font file lives next to the CSS that references it, resolved through esbuild’s file loader: content-hashed, and rewritten to resolve under/dist/client/.
app/assets/app.css
@import '@fontsource/inter' resolves through the project’s own node_modules, the same way any other npm import would:
app/assets/app.css
A root-absolute URL inside CSS, something like
url('/static/logo.png'), is left alone rather than bundled. Fymo treats it as a reference to a verbatim static file, not a build input, so esbuild passes it through untouched.Verbatim files
app/static/ is the other half: files committed to git and served byte-for-byte at /static/<path>, unchanged by the build. A favicon, a PDF, a manifest, anything that should reach the browser exactly as it sits on disk.
Every response from /static/ carries an ETag built from the file’s modification time and size, plus a one-hour Cache-Control. Send that ETag back as If-None-Match and a matching file returns a real 304, body omitted:
.. segment, no absolute-path override, and a final containment check that also catches anything a symlink might try to escape through.
The root allowlist
A handful of filenames are conventionally expected at the bare domain root, not under any prefix, a browser asking for/favicon.ico, a crawler asking for /robots.txt. Fymo resolves these from app/static/ too, just at a different URL than everything else in that directory:
favicon.icofavicon.svgrobots.txtapple-touch-icon.pngapple-touch-icon-precomposed.pngsite.webmanifestbrowserconfig.xml
.well-known/ prefix is allowlisted the same way, for things like ACME challenges or security.txt. Put a file at app/static/robots.txt and it’s served at /robots.txt, not /static/robots.txt.
The allowlist only knows where to look, it doesn’t invent content. If
app/static/robots.txt doesn’t exist, /robots.txt 404s exactly like any other missing route, not a 500 and not a fallback page.Favicon and svelte:head
A freshly scaffolded layout wires its favicon throughsvelte:head, not through getDoc():
app/templates/_layout.svelte
svelte:head and getDoc() divide that work between them.
The app/ directory
Where assets and static files fit among the rest of a Fymo project.
Storage and media
Files written at runtime, and serving them with byte-range support.
