Serve 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
Read https://cicadel.fauzaanu.com/en/0.2.2/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}
}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:
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. Uploadcollectstaticoutput there. Production requireshttps;httpis 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
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>/ 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,
findstaticand source maps are not generated. - The bundled htmx runtime is still served from its own versioned path; see interactive behavior.