Theme Asset Delivery
Coil theme assets are published artifacts with a manifest-backed delivery plan, not raw files that templates reference directly forever.
Start With The Template Call Site
A normal template should look like this:
<link rel="stylesheet" href="/theme/assets/site.css" coil:href="asset('theme/assets/site.css')" />
<script src="/theme/assets/site.js" coil:src="asset('theme/assets/site.js')" defer="defer"></script>
Annotated:
- the literal
hrefandsrcare readable development fallbacks asset('theme/assets/...')is the real contract- the runtime replaces the logical path with the current published URL
That one example explains the whole subsystem better than a list of crates does.
What Problem Is This Solving?
Asset delivery needs to satisfy all of these at once:
- customer apps should use stable logical asset names
- production should serve hashed assets
- local and production templates should not diverge
- the runtime should know which published URL belongs to which logical asset
That is why Coil uses:
- asset roots in the app manifest
- publication plans
- hashed artifact paths
- an active asset manifest during rendering
How Publication Starts
Customer apps opt assets into publication with [theme].asset_roots:
[theme]
asset_roots = ["theme/assets"]
That tells Coil where the logical asset tree begins.
How The Delivery Flow Works
The current delivery flow is:
- the customer app declares asset roots
- those files are turned into deployment artifacts
- each artifact gets a hashed path and fingerprint
- publication produces an active asset manifest
- request rendering injects logical-path to public-URL mappings into the template model
asset('...')resolves through that manifest
The important design choice is step 5: templates do not need to know the hash.
What The Runtime Actually Injects
At render time, the runtime loops through the active asset manifest and adds asset-path bindings to
the RenderModel.
Conceptually, it is doing this:
model = model.with_asset_path("theme/assets/site.css", "https://cdn.example.com/...hashed.css")?;
That is why template code stays small and stable while the published URL can change on every build.
What Delivery Targets Exist?
Coil’s asset model includes these target shapes:
CdnSignedObjectAppProxyLocalPath
That is the general asset capability.
For theme assets specifically, the currently checked-in publication flow is effectively CDN/object store first. In other words:
- theme publication expects a
cdn_base_url - the checked-in demos publish public theme assets as CDN-style URLs
That is an implementation fact the docs should state plainly.
Local Example
If you picture the flow with one CSS file, it looks like this:
logical asset path:
theme/assets/site.css
published artifact path:
theme/assets/site.<fingerprint>.css
render-time resolved URL:
http://localhost:9002/gitly/theme/assets/site.<fingerprint>.css
The template still only says:
coil:href="asset('theme/assets/site.css')"
What Config Is Involved?
App-level:
[theme].asset_roots
Runtime-level:
[assets].publish_manifest = true[assets].cdn_base_url = "..."
Practical consequence:
- if you remove
cdn_base_urltoday without changing the implementation, theme publication breaks
Common Mistakes
Assuming asset('...') means “served by the app process”
It means “resolve this logical asset through the current publication manifest.”
Hardcoding /theme/assets/... as the final production URL
That defeats hashing and manifest resolution.
Treating theme assets as an afterthought compared to managed assets
Theme assets are already part of the deployment contract.
Supporting Implementation And Repo Examples
Concrete supporting files:
apps/shoppr/app.tomlapps/shoppr/platform.dev.tomlapps/gitly/app.tomlapps/gitly/platform.dev.tomlcrates/coil-assets/src/release.rscrates/coil-assets/src/delivery.rscrates/coil-runtime/src/storage/host.rscrates/coil-runtime/src/render/model.rs