5.3 KiB
Vendor Frontend Assets — Implementation Guide
Purpose: Establish the vendored-assets pattern in a project that serves static frontend files through the ontoref daemon (or any Axum-based server with a static file route). Covers the full setup: directory layout, just recipes, and template references.
Substitutions required before use:
{project_dir}— absolute path to project root{daemon_crate}— path to the Axum daemon crate (e.g.crates/myproject-daemon){assets_route}— URL prefix the daemon serves assets from (e.g./assets)
Why vendor instead of CDN
CDN references in server-rendered templates create three failure modes that vendoring eliminates:
- Version drift — a CDN package can be yanked or silently updated at the minor level depending on the version specifier used.
- Offline development — daemon templates break without network access.
- Registration race — some Cytoscape extensions auto-register against
window.cytoscapeat load time; if the CDN is slow or returns a 404 for a sub-resource (e.g. a CSS that does not exist in that package version), the extension silently fails to register andcy.pluginName(...)throws at runtime with no network error visible in the console.
The canonical signal that a CDN asset has a sub-resource problem: curl -sI <url> returns 200 but the body starts with "Couldn't find the requested file".
Always curl -fsSL (fail on HTTP errors) when downloading to catch this.
Directory layout
assets/
vendor/ ← all vendored JS; served at {assets_route}/vendor/<file>
<lib>@<ver>.js ← optional: keep version in filename for cache-busting
justfiles/
assets.just ← recipes for downloading/updating vendor assets
justfile ← root just file; imports assets.just
Vendor files are committed to the repository. They are not gitignored — the goal is reproducible builds without network access.
assets.just structure
# Frontend asset management
#
# Vendored JS libs live in assets/vendor/ and are served by the daemon
# at {assets_route}/vendor/<file>. Pin versions explicitly; bump manually.
LIB_VERSION := "x.y.z"
# Download/update all vendored frontend JS dependencies
[doc("Vendor all frontend JS dependencies")]
vendor-js: vendor-<lib>
[doc("Vendor <lib>")]
vendor-<lib>:
mkdir -p assets/vendor
curl -fsSL \
"https://cdn.jsdelivr.net/npm/<lib>@{{LIB_VERSION}}/<lib>.js" \
-o assets/vendor/<lib>.js
@echo "vendored <lib>@{{LIB_VERSION}}"
Rules:
- One version variable per library, at the top of the file.
vendor-jsis the aggregate recipe — depends on all individualvendor-*recipes.- Use
curl -fsSL(not-sL):-fmakes curl exit non-zero on HTTP errors, catching 404s before they write a garbage file to disk. - Adding a new library: add a version variable, add a
vendor-<lib>recipe, add it as a dependency ofvendor-js.
Importing in the root justfile
import 'justfiles/ci.just'
import 'justfiles/assets.just'
Referencing vendor assets in Tera templates
In {% block head %}, load vendored scripts after their host library:
<!-- Host library (CDN is acceptable for well-established, stable libs) -->
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.30.2/dist/cytoscape.min.js"></script>
<!-- Extension: vendored locally — loads after host, auto-registers on window.cytoscape -->
<script src="{assets_route}/vendor/cytoscape-navigator.js"></script>
Do not use <link rel="stylesheet"> for a vendor asset unless you have verified
the CSS file actually exists in the npm package (curl -fsSI <url> and check Content-Type).
Many Cytoscape extensions ship JS-only and use inline styles; fetching a non-existent
CSS produces a silent 404 that can confuse debugging.
Verifying a new CDN asset before vendoring
# 1. Check the package contents on npm
curl -s "https://registry.npmjs.org/<lib>/<version>" | jq '.dist.unpackedSize, .dist.fileCount'
# 2. Confirm the specific file exists (fail fast)
curl -fsSI "https://cdn.jsdelivr.net/npm/<lib>@<version>/<file>.js" | head -3
# 3. Check auto-registration pattern (for Cytoscape extensions)
curl -sL "https://cdn.jsdelivr.net/npm/<lib>@<version>/<file>.js" | tail -10
# Look for: if (typeof cytoscape !== 'undefined') { register(cytoscape); }
# If absent, the extension requires manual registration: cytoscape.use(require('<lib>'))
Agent steps to apply this pattern
- Read
justfiles/assets.justif it exists; otherwise create it from the structure above. - Add the library version variable and
vendor-<lib>recipe. - Add the new recipe as a dependency of
vendor-js. - Run
just vendor-<lib>to download the file. - Verify the downloaded file is valid JS:
head -3 assets/vendor/<lib>.js— if the first line starts with "Couldn't" or "<!DOCTYPE", the download silently failed; re-run withcurl -fsSL(the-fflag was likely missing). - Update the Tera template: replace the CDN
<script src="...">with<script src="{assets_route}/vendor/<lib>.js"></script>. - Remove any
<link rel="stylesheet">that referenced the same CDN package unless you have verified the CSS exists (step 2 of the verification section above). - Commit
assets/vendor/<lib>.jsandjustfiles/assets.justtogether.