# Static files

Every app publishes its public CSS, images, fonts and scripts through
`App.Static`. Cicadel hashes each file's content, serves it under the app's
namespace and gives you a cache-busting URL. The same files can be collected
into a directory for a CDN or reverse proxy.

## Ask your agent

```text
Read https://cicadel.fauzaanu.com/en/0.2.5/howto/static-files/index.md.
Publish [files] from [app] through App.Static. Resolve each URL in the handler
with static.URL and pass it to the component as a typed prop. Do not add a
route for static files. Run the app tests and go run manage.go collectstatic.
```

## Publish an app's files

Embed a directory and pass it as `App.Static`:

```go
//go:embed all:static
var public embed.FS

func New() cicadel.App {
    files, err := fs.Sub(public, "static")
    if err != nil {
        panic(err)
    }
    return cicadel.App{Name: "notes", Routes: URLs(), Static: files}
}
```

`startapp` generates this layout with an empty `static/` directory. The
starter project publishes `project/static/` as the `project` app, and the
accounts package publishes its stylesheet as the `accounts` app.

A file `static/css/notes.css` in the `notes` app is available at:

| URL | Caching |
| :--- | :--- |
| `/static/notes/css/notes.3f1c9a2b7d4e.css` | `public, max-age=31536000, immutable` |
| `/static/notes/css/notes.css` | `no-cache` with a strong content `ETag` |

The 12-character part is the start of the file's SHA-256 digest, so the URL
changes whenever the content changes. The app name prefix means two apps can
never publish the same path, and a project cannot silently replace a package's
file.

Files and directories starting with `.` are never published. Symbolic links,
duplicate app names and an authored file whose name equals another file's
hashed name are construction errors. `App.Resources` remains private: nothing
in it is served.

## Link to a file

Resolve URLs in the handler and pass them to components as typed props:

```go
func Index(w http.ResponseWriter, r *http.Request) {
    stylesheet, err := static.URL(r.Context(), "notes", "css/notes.css")
    if err != nil {
        response.Error(w, http.StatusInternalServerError)
        return
    }
    page := layout.Page.Call(layout.Props{Title: "Notes", Stylesheet: stylesheet},
        layout.Slots{Children: ui.Text("Notes")})
    if err := (ui.Renderer{}).HTML(r.Context(), w, http.StatusOK, page); err != nil && !ui.Committed(err) {
        response.Error(w, http.StatusInternalServerError)
    }
}
```

`static.URL` fails for an unpublished file instead of producing a broken link.
The application puts its manifest in every request context. Outside a request,
use `app.Static().URL("notes", "css/notes.css")`; tests use the same method to
find the URL to fetch.

## Configure the URL

`settings.StaticURL` (environment: `CICADEL_STATIC_URL`) defaults to
`/static/`.

- **Local path** such as `/static/` or `/assets/v2/`: the application serves the
  files. Requests under the prefix are answered before global middleware runs,
  after host validation and the standard security headers. Registering a route
  under the prefix is a startup error while any app publishes files.
- **Absolute URL** such as `https://cdn.example.com/static/`: URLs point at that
  origin and the application does not serve them. Upload `collectstatic` output
  there. Production requires `https`; `http` is accepted only with debug on.

The value must end in `/` and must not contain a query or fragment.

## Collect files for a CDN or proxy

```sh
go run manage.go collectstatic public
```

`collectstatic` runs offline: it does not open PostgreSQL or start app
services. It writes each authored and hashed file under `public/<app>/` plus a
`public/manifest.json` mapping `app/path` to the hashed path. Files are written
atomically. Earlier hashed files are kept so pages from the previous release keep
working during a rolling deployment; delete old ones yourself when no deployed
release references them. If a hashed path already exists with different bytes,
collection stops with an error.

Serve the hashed files with a long immutable cache lifetime and the authored
names with revalidation, matching the table above.

## Limits

- Source filesystems must not change while the application runs. Embedded files
  are the supported source; the development server restarts after rebuilding
  CSS, which recomputes hashes.
- References inside a file are not rewritten. A stylesheet's relative
  `url(font.woff2)` resolves to the authored name, which is served with
  revalidation rather than an immutable lifetime.
- Precompressed variants, `findstatic` and source maps are not generated.
- The bundled htmx runtime is still served from its own versioned path; see
  [interactive behavior](https://cicadel.fauzaanu.com/en/0.2.5/howto/htmx/index.md).
