Serve static files
Every app publishes its public CSS, images, fonts and scripts through
App.Static. Cicadel serves each file under the app’s namespace with a
cache-busting URL, and can collect the files for a CDN or reverse proxy.
#Ask your agent
Read https://cicadel.fauzaanu.com/en/0.2.7/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: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}
}Generated code already does this:
startappgenerates this layout with an emptystatic/directory.- The starter project publishes
project/static/as theprojectapp. - The accounts package publishes its stylesheet as the
accountsapp.
#How files are served
A file static/css/notes.css in the notes app is available at two URLs:
| 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 |
- Hashed name. The 12 characters are the start of the file’s SHA-256 digest. The URL changes whenever the content changes.
- App prefix. Two apps can never publish the same path. A project cannot silently replace a package’s file.
#What is not published
- Files and directories starting with
.are never published. App.Resourcesstays private. Nothing in it is served.- Symbolic links, duplicate app names, and an authored file whose name equals another file’s hashed name are construction errors.
#Link to a file
Resolve URLs in the handler and pass them to components as typed props:
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/.
It must end in / and must not contain a query or fragment.
| Value | Behavior |
|---|---|
Local path, such as /static/ or /assets/v2/ | The application serves the files. Requests under the prefix are answered after host validation and the standard security headers, but before global middleware. |
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. |
Warning
With a local path, registering a route under the prefix is a startup error while any app publishes files.
#Collect files for a CDN or proxy
go run manage.go collectstatic publiccollectstatic runs offline. It does not open PostgreSQL or start app services.
- It writes each authored and hashed file under
public/<app>/. - It writes
public/manifest.json, mappingapp/pathto the hashed path. - It writes files atomically.
- It keeps earlier hashed files, 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, it 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. That name is served with revalidation, not an immutable lifetime. - Precompressed variants,
findstaticand source maps are not generated. - The bundled htmx runtime is still served from its own versioned path. See interactive behavior.