How to Use the asset! Macro for Static File Bundling and Caching in Topcoat

The asset! macro embeds a compile-time binary descriptor into your executable that the Topcoat bundler extracts to create content-hashed, cache-friendly static files served under /_topcoat/assets.

The asset! macro in the tokio-rs/topcoat repository transforms static files and remote URLs into type-safe, zero-cost handles that are bundled and cached at build time. Unlike traditional file serving that relies on runtime filesystem access, Topcoat's approach embeds asset metadata directly into your binary through the RawAsset encoding system, enabling deterministic content hashing and immutable CDN deployments.

Understanding the asset! Macro Architecture

The asset! macro expands into three components at compile time: a constant Asset handle, an EncodedAsset byte array, and a pre-computed AssetId. This architecture ensures assets cannot be dead-code eliminated while keeping the runtime handle lightweight.

Asset and AssetId

In crates/topcoat-asset/src/asset.rs (lines 17-27), the Asset struct defines a thin handle containing the embedded byte slice and providing an id() method to retrieve the stable AssetId. The AssetId (lines 78-90) is a deterministic 64-bit hash derived from the crate name, source file path, asset path, and options, guaranteeing stability across builds when declarations remain unchanged.

RawAsset Encoding

The RawAsset::encode and RawAsset::decode functions (lines 31-41 and 44-55) manage a fixed-size 2048-byte binary payload containing the ID, path, crate context, and options. The bundler locates these declarations by scanning the binary for a unique scrambled prefix, allowing it to extract every asset definition without parsing source code.

The Bundler

The Bundler implementation in crates/topcoat-asset/src/bundler.rs walks the compiled binary, finds every encoded asset descriptor, resolves source paths (local or remote), copies or downloads files into the bundle directory, and produces a manifest.toml mapping AssetId to bundled filenames. This process runs via the topcoat asset bundle command.

Declaring Assets with the asset! Macro

Local File Paths

Local paths resolve relative to the declaration context:

  • ./ or ../ → relative to the source file containing the asset! call
  • Other relative paths → anchored to CARGO_MANIFEST_DIR
  • Absolute paths → used verbatim
use topcoat::{asset::{Asset, asset}, view::view};

// Relative to CARGO_MANIFEST_DIR
const LOGO: Asset = asset!("assets/logo.png");

// Relative to this source file (src/ui/header.rs)
const HEADER_BG: Asset = asset!("./images/header.jpg");

Remote URLs

Remote assets specified with http:// or https:// are downloaded once during bundling and cached in <target>/topcoat/cache/assets for subsequent builds. The bundler treats cached downloads as local files after initial retrieval.

const DATATABLE_JS: Asset = asset!(
    "https://cdn.jsdelivr.net/npm/datatable@1.10.24/datatable.min.js"
);

Asset Options

The macro accepts optional named arguments compiled into the binary descriptor:

  • rename: "name" → Replaces the file stem in the bundled filename
  • extension: "ext" → Forces a specific extension for files without one
  • checksum: "sha256:<hex>" → Verifies source integrity; fails the bundle if mismatched
  • content_type: "mime/type" → Overrides automatic MIME type detection
const VERIFIED_JS: Asset = asset!(
    "https://cdn.example.com/lib.js",
    rename: "library",
    checksum: "sha256:9f5b2c3d4e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
    content_type: "application/javascript"
);

The Bundling Workflow

The end-to-end asset pipeline follows four stages:

  1. Declaration → Developers call asset! anywhere in the codebase, creating embedded descriptors in crates/topcoat-asset/src/asset.rs (lines 63-66)
  2. Compilation → The Rust compiler embeds the RawAsset binary descriptors into the final executable
  3. Bundling → Running topcoat asset bundle scans the binary, resolves all paths, downloads remote files, computes content hashes, and writes files to <target>/assets with hashed names like logo-1a2b3c4d5e6f7a8b.png
  4. Verification → The bundler generates manifest.toml mapping AssetId values to content-hashed filenames, ensuring the bundle matches the exact binary that produced it

Serving Assets in Production

Local Serving

Attach the bundle to your router using RouterBuilderAssetExt::assets, which registers the route prefix /_topcoat/assets and makes each Asset render as its public URL.

use topcoat::{
    asset::{AssetBundle, RouterBuilderAssetExt},
    router::{Router, RouterBuilderDiscoverExt},
};

pub fn router() -> Router {
    Router::builder()
        .discover()
        .assets(AssetBundle::load().unwrap())
        .build()
}

If the bundle resides outside the executable directory, use AssetBundle::load_dir("dist/assets") to specify the path.

CDN Hosting

For external hosting, use AssetConfig::hosted_at to generate URLs pointing to your CDN while still loading the local bundle for ID-to-filename resolution.

use topcoat::{
    asset::{AssetBundle, AssetConfig, RouterBuilderAssetExt},
    router::Router,
};

let router = Router::builder()
    .assets(AssetConfig::hosted_at(
        "https://cdn.example.com/assets",
        AssetBundle::load().unwrap(),
    ))
    .build();

This configuration emits URLs like https://cdn.example.com/assets/logo-1a2b3c4d5e6f7a8b.png. You must manually upload the bundle files from <target>/assets to your CDN.

Summary

  • The asset! macro creates compile-time asset descriptors that survive dead-code elimination and provide zero-cost runtime handles.
  • Path resolution supports relative paths (to source file or manifest), absolute paths, and remote URLs with local caching.
  • Asset options enable renaming, extension overrides, SHA-256 verification, and content-type specification.
  • The Bundler extracts descriptors from the binary, hashes content, and produces cache-friendly filenames.
  • Router integration via RouterBuilderAssetExt::assets serves files locally or generates CDN URLs while maintaining the AssetId mapping.

Frequently Asked Questions

What happens if I deploy a new binary without updating the asset bundle?

The application will panic at runtime when attempting to render an asset. Because AssetId values are derived from the declaration location and options in crates/topcoat-asset/src/asset.rs (lines 78-90), the binary and bundle share a strict coupling. This early failure prevents broken image links or stale JavaScript in production.

How does Topcoat handle cache invalidation for static assets?

The bundler automatically embeds a content hash into every filename (e.g., app-5e6f7a8b9c0d1e2f.css). Since the hash changes only when the file contents change, you can serve these assets with immutable, long-lived HTTP cache headers. When content changes, the AssetId maps to a new hashed filename, forcing clients to fetch the updated version.

Can I use the asset! macro with files outside my crate directory?

Yes, but with limitations. Absolute paths are used verbatim, and relative paths resolve to CARGO_MANIFEST_DIR. However, for reproducible builds and team development, it is recommended to keep assets within the crate directory or use remote URLs for external dependencies. The bundler caches remote downloads in <target>/topcoat/cache/assets to avoid repeated network requests.

What is the performance impact of using many asset! declarations?

Runtime performance impact is zero. Each Asset is a const handle containing a static byte slice reference. The compile-time binary size increases by 2048 bytes per asset (the RawAsset descriptor size), and the bundler scans this embedded metadata during the build process, not at runtime. Memory overhead consists only of the Asset handle (typically a pointer and length) when used in views.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →