Skip to content

Asset pipeline

The asset pipeline fingerprints assets with a content hash, records the mapping in a manifest, and resolves the view helpers through it so a changed asset gets a new URL. It also builds import maps and serves digested assets with a long cache lifetime.

Fingerprinting and the manifest

digest-for($content) is the content hash; digested-name('app.css', $content) inserts it before the extension (app-<digest>.css). A Manifest maps each logical path to its digested name:

1
2
3
4
use MVC::Keayl::Assets;

my $manifest = MVC::Keayl::Assets::Manifest.build('app/assets'.IO, output => 'public/assets'.IO);
$manifest.lookup('css/app.css');   # 'css/app-<digest>.css'

build walks the source tree, digests each file, writes the digested copy to the output directory, and records the mapping. to-json/from-json persist the manifest.

Resolving in views

The asset helpers take a resolver. manifest-resolver($manifest) resolves a logical name to its digested /assets/... path, passing absolute and external URLs through unchanged:

1
2
3
4
my &resolver = manifest-resolver($manifest);

stylesheet-link-tag('application', :&resolver);   # href="/assets/application-<digest>.css"
image-tag('logo.png', :&resolver);                 # src="/assets/logo-<digest>.png"

set-asset-manifest($manifest) registers a manifest globally, and digested-resolver is a resolver that consults it, so helpers can resolve without threading the manifest through each call:

1
2
set-asset-manifest($manifest);
image-tag('logo.png', resolver => &digested-resolver);

Serving

MVC::Keayl::Assets::Serving::AssetsController serves digested files from the configured root with Cache-Control: public, max-age=31536000, immutable (safe because the digest changes when the content does). It rejects path traversal and returns 404 for unknown assets:

1
set-asset-root('public/assets'.IO);

Import maps

An ImportMap pins module names to URLs. pin adds one (defaulting the URL to /assets/<name>.js); pin-all-from pins every .js file under a directory:

1
2
3
4
my $importmap = MVC::Keayl::Assets::ImportMap.new;
$importmap.pin('application', preload => True);
$importmap.pin('lodash', to => 'https://cdn/lodash.js');
$importmap.pin-all-from('app/javascript/controllers'.IO, under => 'controllers');

javascript-importmap-tags($importmap) emits the <script type="importmap"> with the import JSON and a <link rel="modulepreload"> for each preloaded pin. Pass a manifest to resolve the module URLs to their digested paths:

1
javascript-importmap-tags($importmap, :$manifest);

Precompiling

keayl assets-precompile builds the manifest and digested files, writing them and a manifest.json to public/assets:

1
keayl assets-precompile

It reads from app/assets and reports the count, or fails when there is no asset source directory.