Skip to main content
Every Fymo app splits files two ways: things the build compiles, and things it copies untouched. 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.
If you’ve used Rails, this maps directly onto app/assets and public: one is compiler input, the other is copied verbatim. Fymo just calls the second one app/static.

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 plain import in its <script> block:
app/templates/_layout.svelte
esbuild bundles that CSS into the layout’s own entry, and a page links whatever its layout chain pulls in, root first, then anything a nested layout adds. A resource’s own _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:
Any other loose .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
A package-based font works too. @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.
The old /assets/ URL prefix is gone. It doesn’t redirect and it doesn’t dual-serve alongside /static/, it simply stops existing: a request to the old prefix falls through to routing and hits a normal 404. Grep your app for /assets/ if you’re migrating an older project.
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:
Path resolution is traversal-guarded the same way storage keys are: no .. 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.ico
  • favicon.svg
  • robots.txt
  • apple-touch-icon.png
  • apple-touch-icon-precomposed.png
  • site.webmanifest
  • browserconfig.xml
The .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 through svelte:head, not through getDoc():
app/templates/_layout.svelte
That’s the general pattern for markup that’s the same on every page: a favicon link, a font preconnect, anything a controller doesn’t need to decide per request. See Controllers and routing for how 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.