Static assets
Datapages can compile stylesheets, scripts, fonts and images into the server binary. The generated packages build their URLs, and the server handles caching and compression.
Declare the files
Put the files in a directory of the app package, such as app/static/. Declare an embed.FS variable whose doc comment names the URL path to serve them at:
// StaticFS is /static/
//
//go:embed static/*
var StaticFS embed.FS
The doc comment sets the URL prefix the way the doc comment of a page sets its route. datapages gen and datapages lint reject a declaration that breaks these rules:
- An app package declares at most 1 such variable.
- The prefix starts and ends with
/and is not/. It contains only ASCII letters, digits,-,.,_,~and/. - The directive names exactly 1 directory inside the app package.
static/*.cssnames files, not a directory. - No page or action route starts with the prefix, which reserves every path below it.
Serve the files
Pass the variable to the server in cmd/server/main.go. datapages init and datapages gen write that file only when it is missing, and neither adds the option to an existing one:
opts = append(opts, datapages.WithAssets(app.StaticFS, false))
false answers 404 for a directory without an index.html instead of listing its files. The generated server serves the static directory of the embed.FS below the prefix, as app/static/style.css at /static/style.css. Without the option, every path below the prefix receives the 404 page. datapages.NewServer returns an error for WithAssets when the app package declares no assets.
datapages.WithAssetsFS takes an http.FileSystem in place of the embed.FS and serves it below the declared prefix as it is, without extracting a subdirectory and also in dev mode.
Set cache headers
Assets carry Cache-Control: public, max-age=0 and an ETag computed from the file contents. The browser asks before every reuse, sends the ETag back in If-None-Match and receives 304 Not Modified without a body while the file is unchanged. datapages.WithAssetsCache changes these headers.
MaxAge lets browsers reuse files without asking. It applies to every file below the prefix. Set it only when every file name changes with its content, as datastar-1.0.4.js or app.3f9a1c.css does. Otherwise browsers keep old files after a deploy until MaxAge ends. For such names, let browsers keep files for a year:
datapages.WithAssetsCache(datapages.AssetsCacheConfig{
MaxAge: 365 * 24 * time.Hour,
Immutable: true,
}),
This sends Cache-Control: public, max-age=31536000, immutable. Immutable keeps a reload from revalidating a fresh file and requires a positive MaxAge. CacheControl sets the header value as written and excludes MaxAge and Immutable. A value with no-transform keeps the assets uncompressed. DisableETag omits the ETag, for a CDN or a compressing middleware that writes its own.
Write asset URLs
Run datapages gen after declaring the variable and before any template calls the generated functions. It generates 2 functions that return the URL of a file:
assets.Pathin the packageapp/datapagesgen/assetshref.Assetin the packageapp/datapagesgen/href
Both return /static/style.css for "style.css" and percent-encode the path. Use href.Asset in an <a href>, where datapages lint accepts only calls into the href package. Use either in other elements and in Go code:
templ head() {
<link rel="stylesheet" href={ assets.Path("style.css") }/>
<script type="module" src={ assets.Path("app.js") }></script>
}
templ downloadLink() {
<a href={ href.Asset("report.pdf") } download>Download the report</a>
}
App.Head adds tags to the <head> of every page:
func (*App) Head(r *http.Request) datapages.Head {
return head()
}
datapages lint checks the URLs of <a href> and of Datastar actions, not those of <link>, <script> or <img>. A hardcoded /static/style.css there passes the linter and breaks when the prefix changes.
Spell each path exactly as its file is named. A request reaches a file only when each element of its path matches the directory listing byte for byte. /static/App.js receives 404 for the file app.js on every system and in dev mode, also on a file system that ignores case, such as the default one of macOS. See Asset Paths.
Serve files that change
Outside dev mode, the server hashes a file on its first request. It keeps the hash, ETag and compressed copies until the process exits. Files passed through WithAssets cannot change because they are compiled into the binary.
Do not change files served from a directory through WithAssetsFS while the server runs unless asset compression and the ETag are both off. A changed file keeps its old ETag, and a compressed response can keep the old content until restart. See Changing Assets.
Serve files that change at run time, such as uploads, through a GET action that returns datapages.File:
// GETLogo is /logo.png
func (a *App) GETLogo(r *http.Request) (file datapages.File, err error) {
f, err := a.Uploads.Open("logo.png")
if err != nil {
return datapages.File{}, datapages.ErrNotFound
}
info, err := f.Stat()
if err != nil {
return datapages.File{}, errors.Join(err, f.Close())
}
return datapages.File{Type: "image/png", Body: f, ModTime: info.ModTime()}, nil
}
Uploads is an *os.Root field of App. The response carries Cache-Control: no-cache and Last-Modified. The browser revalidates with If-Modified-Since and receives 304 while the file is unchanged. href.App.Logo() returns the URL. See Actions for GET actions and datapages.File for its fields.
Self-host the Datastar bundle
Pages load the Datastar bundle from https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.4/bundles/datastar.js by default. Serve it from the app's assets when pages must not depend on another origin. Download it into the assets directory:
curl -fsSL -o app/static/datastar-1.0.4.js \
https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.4/bundles/datastar.js
Pass its URL to the server:
opts = append(opts, datapages.WithDatastarJS(assets.Path("datastar-1.0.4.js")))
Keep the version in the file name. Browsers and the offline service worker cache files by URL. If an upgrade keeps the name, they can keep using the old file from their cache. A new name is a new URL, which they download. WithDatastarJS accepts an http, https or relative URL. WithCSPNonce requires Datastar 1.0.3 or later. See Security for the Content-Security-Policy.
Dev mode
Dev mode is on when DATAPAGES_DEV_MODE or TEMPL_DEV_MODE is not empty. datapages watch sets TEMPL_DEV_MODE before it starts the server. In dev mode the server:
- reads the files from the source directory,
./app/staticfor the declaration above. The path is relative to the working directory of the server, whichdatapages watchsets to the module root. An edited, added or renamed file is served without a rebuild. - sends
Cache-Control: no-cachewithoutETagorLast-Modified. The browser downloads the file again on every use. - ignores
WithAssetsCache,WithResponseCompressionandWithAssetsCompression, and sends assets and pages uncompressed. - logs the warning
dev mode is onat startup.
A production process that inherits either variable reads its assets from a directory it does not have and answers 404. The generated constant assets.DevDir holds the path. See Dev Mode.
Compression
Outside dev mode, Datapages compresses an eligible asset of at least 256 bytes on its first request. It keeps the copy in memory until the server exits. It uses brotli 6, or gzip 9 when the request does not accept brotli.
Eligible types include text, JavaScript, JSON, XML, YAML, TOML, SVG, WebAssembly, TrueType and OpenType fonts, icons and BMP images. PNG, JPEG, WOFF2 and other precompressed formats stay uncompressed. datapages.WithNoAssetsCompression turns compression off. Performance explains precompression, other levels and disk storage.
See also
- Asset Paths, Changing Assets and Dev Mode: the exact rules for paths, changed files and dev mode.
- Asset Compression: the eligible types and the headers of compressed responses.
- Performance: precompression, compression levels and the disk store.
- Offline and shims: precache the assets that offline pages need.
- Actions: GET actions that serve files.
WithAssetsandAssetsCacheConfig: the Go API.