cicadel

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

Text
Read https://cicadel.fauzaanu.com/en/0.2.8/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}
}

Generated code already does this:

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

#How files are served

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

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

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/. It must end in / and must not contain a query or fragment.

ValueBehavior
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

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>/.
  • It writes public/manifest.json, mapping app/path to 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, 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.