# Assets Images, fonts, PDFs, downloads. Any file your app ships that is not code. There are two kinds, and they are not the same problem: - **Static assets** live in your source tree. You know their names when you write the code. They are content-hashed at build time and served from `/assets/`. - **Dynamic assets** come from the database: a product photo, an avatar, an upload. You do not know the filename until the row is read, so a build-time URL cannot exist. These are served by a route. ## Static assets Put them anywhere under `app/`. The scaffold uses `app/shared/assets/`. ``` app/shared/assets/ favicons/favicon.svg products/quad-lineup.jpg ``` ### In markup, write a path ```ehtml ``` Relative paths only. The build rewrites the attribute to the file's content-hashed URL: ```html ``` This works on `src`, `href`, `poster` and `data`, and on the `content` of the meta tags that hold URLs (`og:image`, `twitter:image`, and the rest of that family). `content` is a generic attribute name, so the URL-bearing properties are a fixed list rather than anything inferred. It also works in CSS: ```css .hero { background-image: url("../assets/products/quad-lineup.jpg"); } ``` ### In code, import it An import of a non-code file gives you its URL as a `string`: ```ts import favicon from "#app/shared/assets/favicons/favicon.svg"; favicon; // "/assets/app/shared/assets/favicons/favicon.f9e9effe4ae761bd.svg" ``` An import is the only way to get an asset URL into a value. You need one the moment the asset is part of data rather than one fixed spot in markup: ```ts import quad from "#app/shared/assets/products/quad.jpg"; import sidecar from "#app/shared/assets/products/sidecar.jpg"; const CASES = [ { name: "Quad", photo: quad }, { name: "Sidecar", photo: sidecar }, ]; ``` ```ehtml
  • {c.name} {c.name}
  • ``` The URL is inlined at build time, so there is no runtime lookup and no helper to call. An imported asset and a written path produce the identical URL. They are one mechanism with two entry points. Importing an asset makes it servable. That holds from server-only code too, so a route that builds a webmanifest or an rss feed can import an icon and get a real URL, and it holds even when nothing routes to the importing file: the import is the reference, not the reachability of the code around it. **Which to use:** a path when the file sits next to the template that uses it, an import for anything shared or anything that has to travel as a value. The scaffold imports its favicon because `app/shared/assets/` is nowhere near `app/pages/home/`. ### `#` is for imports, not for URLs `#app/...` is import-map syntax. It belongs in an `import` statement: ```ts import favicon from "#app/shared/assets/favicons/favicon.svg"; ``` In an attribute or a CSS `url()`, `#` means what it means in those languages. A page anchor, an SVG fragment. It is left alone: ```ehtml Pricing ``` ```css .icon { fill: url(#gradient); } /* an SVG paint server */ ``` ### Bare imports A clause-less import of an asset binds nothing, so there is nothing to use and the statement is dropped: ```ts import "#app/shared/assets/logo.svg"; // no effect ``` Only `.css` does something useful bare. It links the stylesheet: ```ts import "./style.css"; ``` ### Web fonts Elements emits fonts and hashes their URLs like any other asset, and it does not preload them. Stylesheets are preloaded automatically because `@import` is serial per level and the browser cannot see an interior stylesheet until its parent has downloaded. Fonts are different: a `@font-face` is a declaration, not a fetch, and `unicode-range` makes it conditional. Only the page knows which faces it paints. So write the preload yourself, for the faces the first screen sets type in. Import the font so the hint points at the same hashed URL the stylesheet asks for, and keep `crossorigin` on it. Fonts are fetched in CORS mode even same-origin, and a preload without it downloads the file a second time: ```ts import interRoman from "#app/shared/styles/fonts/Inter.subset.woff2"; ``` ```ehtml ``` Faces you do not preload still work. They are fetched when a rule needs them, which is what you want for an italic used below the fold or a full face sitting behind a `unicode-range` miss. ## Dynamic assets When the filename lives in a database row, no build-time URL exists. Store the bytes and serve them from a route. Store the file, with its content type and a hash you can cache on: ```sql create table photos ( id uuid primary key default uuidGenerateV7(), bytes bytea not null, mimeType text not null, hash text not null ); ``` Serve them from a route. A route that returns bytes is not a page, so it lives in `app/routes/`: ```ts // app/routes/photos.ts import { Request, Response, sql } from "@elements/app"; interface Photo { bytes: Buffer; mimeType: string; } export default function servePhoto(req: Request, res: Response) { let id = req.params.id; let photo = sql(` select bytes, mimeType from photos where id = ${id}::uuid `).first(); if (!photo) { res.status(404); return ""; } res.setHeader("Content-Type", photo.mimeType); res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); return photo.bytes; } ``` ```ts // index.ts app.route("/photos/:id", servePhoto); ``` Then the URL is ordinary data: ```ehtml {row.name} ``` The `?v=` is what makes `immutable` safe: the URL changes when the bytes change, so a browser can hold the old one forever without ever showing stale content. That is the same trick the static pipeline plays with a hashed filename. For uploads, accepting the file in the first place, see `elements man recipes/file-upload`.