# 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 ```html ``` 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 }, ]; ``` ```html
  • {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: ```html 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"; ``` ## 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: ```html {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`.