Asset pipeline: fingerprinting & minification

Static assets — CSS, JS, images — want two things before they ship: a cache-busting name so browsers can cache them forever, and fewer bytes so they download fast. The akurai-assets crate does both in pure Rust, zero dependencies, correctness first.

Fingerprinting (cache-busting)

A fingerprint is a short content hash of an asset's bytes. Same bytes → same hash; change a single byte → a different hash. Fold that hash into the filename and the URL itself becomes the cache key:

use akurai_assets::{fingerprint, hashed_name};

let bytes = b".box{color:red}";
let hash  = fingerprint(bytes);          // e.g. "9a1c0f3e7b2d8c41" (16 hex chars)
let name  = hashed_name("app.css", bytes); // "app.9a1c0f3e7b2d8c41.css"

The hash is inserted before the extension (app.cssapp.<hash>.css); multi-dot names keep their stem (vendor.min.jsvendor.min.<hash>.js) and extensionless names just get it appended (LICENSELICENSE.<hash>).

Because the name changes whenever the content changes, a fingerprinted file can be served with an immutable, far-future cache header and never re-validated:

Cache-Control: public, max-age=31536000, immutable

The hash is a 64-bit FNV-1a digest rendered as lowercase hex. It is not cryptographic and does not need to be — cache-busting only needs determinism and good change-sensitivity, not collision resistance against an adversary.

Minification (fewer bytes)

Both minifiers are conservative: they only remove bytes that are provably insignificant, and they never reorder, rename, or rewrite code. Correctness beats ratio.

minify_css

let out = akurai_assets::minify_css(".box {\n  margin: 0 24px 60px;\n}");
// ".box{margin:0 24px 60px}"

{ } : ; , or at the edges).

0 24px 60px shadows are preserved, and the contents of string literals and url(…) (including data: URIs with their own ;, ,, :) pass through byte for byte.

minify_js

let out = akurai_assets::minify_js("let a = 1; // a comment");
// "let a = 1;"

A deliberately minimal pass:

Semicolon Insertion is unaffected.

string (e.g. a URL) is preserved.

Limits (by design): no regex-literal awareness and no ${…} interpolation parsing — full JS minification is risky, so this stays a safe whitespace/comment pass. For aggressive minification, pre-minify upstream and let this layer only fingerprint.

How the CLI uses it

The pipeline is wired into akurai serve. On startup the server scans every static asset under your frontend dir (anything with a non-.html extension), conservatively minifies CSS and JS (other types pass through byte-for-byte), content-hashes the processed bytes, and builds an in-memory manifest mapping each plain path to its fingerprinted name. Nothing is written to disk and your source files are never modified. The generated utilities.css (see Styling) is folded into the same manifest.

Serving rules (backward compatible)

(/styles.<hash>.css) is served from memory with the processed (minified) bytes, the correct Content-Type, and Cache-Control: public, max-age=31536000, immutable — the URL itself is the cache key, so the response can be cached forever.

the raw file is read off disk with no immutable header. A project that never references a fingerprinted URL is completely unaffected.

a page, template, or tool can discover the fingerprinted URLs to link.

Discovering hashed URLs

GET /api/_assets
{ "/styles.css": "/styles.9a1c0f3e7b2d8c41.css",
  "/app.js":     "/app.b4d2…​.js",
  "/utilities.css": "/utilities.…​.css" }

Link the value to opt a given asset into immutable caching; link the plain path to keep the current (revalidated) behavior.

Dev mode

Under akurai dev the pipeline is disabled (an empty manifest): dev wants fresh, un-hashed assets and live-reload, not far-future caching. /api/_assets returns {} and every asset is served plainly off disk.

Status

Shipped: fingerprinting, both minifiers (akurai-assets), and the crates/cli wiring above (startup manifest, immutable cache headers on fingerprinted requests, /api/_assets).

Deferred (by design): the server does not rewrite references inside served HTML to the hashed URLs — automatic rewriting of arbitrary template output is risky, so opting an asset into immutable caching is an explicit choice (link the /api/_assets value). Minification stays the conservative whitespace/comment pass documented above; aggressive transforms are out of scope.