# 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}
```
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
```
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`.