Asset Fingerprinting
Asset fingerprinting appends a content hash to CSS and JS filenames, producing immutable URLs that CDNs and browsers can cache indefinitely. When the file content changes, the hash changes, and browsers automatically fetch the new version. It applies to accent build output and, with the same configuration, to accent serve --production .
Quick Start
- Enable fingerprinting in
config.yaml:
build: fingerprint: enabled: true
- Use the
| fingerprintfilter in your templates:
{% for css in theme.assets.css %}
<link rel="stylesheet" href="/{{ ('theme/assets/' ~ css) | fingerprint }}">
{% endfor %}
- Build your site:
accent build --output ./public --base-url https://example.com
The output will contain both the original and fingerprinted files:
public/theme/assets/css/
main.css # Original (kept for compatibility)
main.64aee5ca8b.css # Fingerprinted copy
How It Works
During accent build, after theme assets are copied and Sass/SCSS styles are compiled:
- The build walks
output/theme/assets/and matches files against the include/exclude patterns. - For each matched file, it computes the SHA-256 hash of the file contents.
- It copies the file with a fingerprinted name:
{stem}.{hash}.{ext}. - A manifest maps original paths to fingerprinted paths.
- The
| fingerprinttemplate filter resolves paths via this manifest during page rendering.
Original files are kept alongside fingerprinted copies so that external links and bookmarks continue to work.
You can build into the same output directory again without --clean: the pass recognises the copies an earlier build wrote and never hashes them as sources. When an asset changed, the copy with the old hash is removed and a copy with the new hash is written, so the directory holds one fingerprinted copy per asset however often you build into it. Pages embed the fingerprinted URL, so a build after an asset change renders every page again, incremental or not.
Template Filter
Basic Usage
The | fingerprint filter takes an asset path and returns the fingerprinted equivalent:
<link rel="stylesheet" href="/{{ 'theme/assets/css/style.css' | fingerprint }}">
{# Renders: /theme/assets/css/style.a1b2c3d4e5.css #}
<script src="/{{ 'theme/assets/js/app.js' | fingerprint }}"></script>
{# Renders: /theme/assets/js/app.deadbeef01.js #}
If the path is not in the manifest (e.g., the file was excluded or fingerprinting is disabled), the filter returns the path unchanged. This means templates work correctly in both fingerprinted and non-fingerprinted builds.
Subresource Integrity (SRI)
Pass integrity=true to get an object with .url and .integrity attributes:
{% set css = 'theme/assets/css/style.css' | fingerprint(integrity=true) %}
<link rel="stylesheet"
href="/{{ css.url }}"
integrity="{{ css.integrity }}"
crossorigin="anonymous">
The .integrity value is a base64-encoded hash in SRI format (e.g., sha256-abc123...), suitable for the HTML integrity attribute. Browsers will verify the file contents match the hash before applying the stylesheet or executing the script.
Serve Mode
Serve behavior depends on the mode:
- Development serve: the
| fingerprintfilter is a no-op and returns the original path unchanged. Dev mode already sendsCache-Control: no-storeon every response, so cache busting would add nothing, and hashed URLs would churn on every asset edit under hot reload. - Production serve (
accent serve --production): whenbuild.fingerprint.enabledistrue, the server computes the fingerprint manifest in memory at startup — no files are renamed or written. The| fingerprintfilter emits the same hashed URLs as a static build, and the server resolves those hashed URLs back to the source assets on each request. Reloading the configuration (SIGHUP orPOST /_admin/reload) recomputes the manifest, so a theme update produces new hashed URLs without a restart.
In production serve, styles that go through the style pipeline (Sass compilation, minification) are hashed after processing, so SRI integrity values always match the bytes the server sends.
That guarantee holds across configuration reloads too. A production server always serves the processed form of an asset, so a reloaded config.yaml that enables development settings cannot make it send unminified CSS under a hashed URL whose integrity was computed from the minified bytes — which browsers would reject outright.
Two cache policies apply in production serve when fingerprinting is enabled:
| URL | Cache-Control |
|---|---|
Hashed URL (style.a1b2c3d4e5.css) | public, max-age=31536000, immutable |
Original URL (style.css) | public, no-cache (revalidates via ETag) |
The hashed URL is content-addressed, so the year-long immutable policy is safe: when the file changes, the URL changes. The original URL stays available for external references but revalidates on each use, so it can never pin a stale stylesheet. A static host has no fingerprint manifest to tell the original URL apart, so accent serve-static and the generated header artifacts keep it for an hour, like any other unfingerprinted asset.
Every other CSS, JavaScript, WebAssembly, and font file is kept for an hour at most: a theme asset outside the fingerprint set, every theme asset while fingerprinting is off, and the search bundle under /_search/. Their URLs stay the same when they change, so a year-long policy would keep a returning visitor on the old file after an upgrade. They get the configured http_headers.cache.defaults.assets or fonts policy without immutable, and with a max-age or s-maxage above a day capped at 3600; every other directive, such as private, stays. A policy of a day or less without immutable applies to them unchanged.
Configuration
All settings are under build.fingerprint in config.yaml:
build: fingerprint: enabled: true # Default: false (opt-in) algorithm: sha256 # sha256, sha384, or sha512 length: 10 # Hex characters in the filename hash include: # Glob patterns to fingerprint - "**/*.css" - "**/*.js" exclude: # Glob patterns to skip - "**/*.map"
Settings Reference
| Setting | Default | Description |
|---|---|---|
enabled | false | Enable asset fingerprinting. Opt-in to avoid breaking existing themes. |
algorithm | sha256 | Hash algorithm: sha256, sha384, or sha512. |
length | 10 | Number of hex characters from the hash to use in filenames. |
include | ["**/*.css", "**/*.js"] | Glob patterns for files to fingerprint. |
exclude | ["**/*.map"] | Glob patterns for files to skip. |
Including Additional File Types
To fingerprint fonts and images as well:
build: fingerprint: enabled: true include: - "**/*.css" - "**/*.js" - "**/*.woff2" - "**/*.svg"
CDN Cache Headers
When fingerprinted assets are deployed behind a CDN, set long-lived cache headers:
Cache-Control: public, max-age=31536000, immutable
This tells browsers and CDNs to cache the file for one year without revalidation. Since the filename changes when the content changes, stale copies are never served.
In accent serve --production, Accent CMS sets these headers itself : hashed URLs get the immutable assets default, and every other asset URL a shorter policy (see Serve Mode above). The .htaccess and _headers files that build.header_artifacts generates follow the same rule: the configured policy for each fingerprinted file and the hour-long policy for the rest. They name up to 200 fingerprinted files; _headers names fewer when the file would pass its 100-rule limit, and a build logs a warning for every file left out. accent serve-static pins every fingerprinted file the build records, as accent serve does. Without those artifacts, configure the headers in your hosting platform, for the hashed names only:
- Cloudflare Pages: Automatic for static assets
- Netlify: add
netlifytobuild.header_artifacts, which writes a rule per fingerprinted file - nginx:
location ~ "\.[0-9a-f]{10}\.(css|js)$" { expires 1y; add_header Cache-Control "public, immutable"; }, with the hash length matchingbuild.fingerprint.length
Edition Availability
Asset fingerprinting requires no license key.