Asset pipeline
gebweb.useStaticAssets(app, dir, opts?) mounts a static-file
serving directory under a URL prefix and gives templates a content
-hashed asset() filter for cache-busting.
A fingerprinted URL like /assets/app-7b2f9c4d.css carries an
eight-character SHA-256 hash of the file content. When the file
changes, the hash changes, so the URL changes, so browsers and CDNs
treat it as a fresh resource. That is the whole reason fingerprinting
exists: it makes Cache-Control: max-age=31536000, immutable safe.
Setup
import gebweb;
let app = gebweb.app([HomeController()]);
gebweb.useViews(app, "templates");
gebweb.useStaticAssets(app, "public");
By default the directory is walked once at startup. Every file gets hashed and indexed under a manifest so the URL helper is a single dict lookup at render time.
Options
gebweb.useStaticAssets(app, "public", {
"urlPrefix": "/assets",
"cacheControl": "public, max-age=31536000, immutable",
"fingerprint": true,
"dev": false
});
urlPrefix- the public mount point. A request under this prefix that matches an existing asset file is served directly, ahead of controller routing, so avoid registering controller routes under the asset prefix.cacheControl- theCache-Controlheader sent with every asset response. The default is appropriate for fingerprinted assets.fingerprint- set tofalseto serve unhashed names verbatim. Useful for legacy paths or when the front-end build system already fingerprints.dev- in dev modeassetreturns the plain logical URL (no content hash), which maps straight to the source file on disk, so edits are picked up live without recomputing a manifest or re-rendering templates.
In dev and non-fingerprint mode the handler resolves the requested file,
following symlinks, and refuses anything that escapes the configured
asset root with a 404. A symlink inside the root that points outside it,
or a .. traversal, is rejected; a filename that merely contains ..
(for example my..file.css) is served normally. A path that cannot be
resolved at all (a removed symlink target, or an asset root that does not
resolve) also returns 404 rather than erroring. Fingerprint mode already
serves only files in the manifest built at startup, so it is unaffected.
Rendering URLs
The asset filter is auto-registered as a view-engine filter when
you call useStaticAssets. In a template:
<link rel="stylesheet" href="{{ 'app.css' | asset }}">
<img src="{{ 'img/logo.png' | asset }}">
Output:
<link rel="stylesheet" href="/assets/app-7b2f9c4d.css">
<img src="/assets/img/logo-a1b2c3d4.png">
If the requested logical name is not in the manifest, the filter
returns the name under the mount prefix (urlPrefix + name), so a
typo shows up as a broken link under the asset path rather than
crashing the render.
Subdirectories
Directory structure is preserved in the URL. public/img/logo.png
maps to /assets/img/logo-<hash>.png. The full relative path is
the manifest key, so name collisions across directories are
impossible by design.
Content types
The handler guesses the Content-Type header from the file
extension. Common web extensions (.css, .js, .html, .svg,
.png, .jpg, .gif, .webp, .ico, .woff, .woff2, .json,
.txt, .xml) are recognised. Unknown extensions fall back to
application/octet-stream.
Assets stream straight from disk rather than being buffered in memory,
and the server answers HEAD, Range (206 Partial Content), and
conditional (If-None-Match / If-Modified-Since, 304 Not Modified)
requests automatically alongside the long-cache header.
Build pipeline and bundling
gebweb build can compile, minify, and embed your assets and templates into the
single-binary release so the deployed binary is self-contained. Declare an
assets: block in geblang.yaml:
name: myapp
source: src
assets:
sourceDir: assets # raw JS/TS/JSX/CSS/SCSS sources
outDir: build/assets # compiled output (this is what you serve)
entryPoints:
- app.ts
- app.scss
templatesDir: templates # optional, default "templates"
publicDir: public # optional, default "public"
At build time each entry point is processed:
.js/.jsx/.ts/.tsxare bundled, tree-shaken, and minified with esbuild intooutDir/<name>.js..cssis bundled (resolving@import) and minified with esbuild intooutDir/<name>.css..scss/.sassare compiled with dart-sass (must be onPATH), then minified, intooutDir/<name>.css.- HTML templates under
templatesDirare minified (template tags such as{{ }}and{% %}are preserved).
The compiled outDir, the (minified) templates, and publicDir are then
embedded in the binary. At run time the framework resolves them through
sys.bundleDir(): a built binary serves the embedded copies, while gebweb dev
and a normal geblang run serve the files from disk unchanged. Your code path
is identical in both cases:
gebweb.useViews(app, "templates");
gebweb.useStaticAssets(app, "build/assets");
Flags:
--no-minifyskips minification (assets and templates) for faster, readable builds.--no-sassskips SASS compilation when dart-sass is not installed. Without it, a.scss/.sassentry point with no dart-sass onPATHfails the build with an actionable message.
gebweb dev compiles the asset entry points once (unminified) so the dev server
serves them from disk; restart the dev server to recompile after editing an
asset source.
Production deployment
The simplest production path is gebweb build: the asset pipeline above
compiles, minifies, and embeds everything, and you ship one binary.
If you prefer to manage assets yourself:
- Have your build pipeline (Sass, esbuild, etc.) write final
files into
public/. - Call
useStaticAssets(app, "public")once at startup; the manifest is built before the first request lands. - Put a reverse proxy or CDN in front for further caching; the
immutabledirective makes this safe at any depth.
For static-only sites, you can also pre-generate the manifest
offline and skip the framework entirely. The fingerprinting logic
is exposed as three helpers in gebweb.assets:
assets.newConfig(sourceDir, opts)builds anAssetConfigwith the manifest computed.assets.assetUrl(cfg, "app.css")returns the fingerprinted URL the same way the view filter does.assets.makeAssetHandler(cfg)returns the route handler the framework mounts. A custom build script can call this directly to produce a static manifest file.