cicadel

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

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:

URLCaching
/static/notes/css/notes.3f1c9a2b7d4e.csspublic, max-age=31536000, immutable
/static/notes/css/notes.cssno-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.

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

Terminal
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.

Type to search this version.