use std::collections::HashMap; use std::sync::LazyLock; use sha2::{Digest, Sha256}; /// A single static asset: its embedded bytes and a content hash used to /// version its URL. pub struct StaticAsset { pub data: &'static [u8], pub hash: String, } /// A registry of the app's static assets, keyed by filename. Each asset's /// content hash is computed once on first use and cached, so the versioned URL /// changes automatically whenever the underlying file changes. pub struct StaticAssetStore { assets: HashMap<&'static str, StaticAsset>, } impl StaticAssetStore { fn new(entries: &[(&'static str, &'static [u8])]) -> Self { let assets = entries .iter() .map(|(name, data)| { let hash = content_hash(data); (*name, StaticAsset { data, hash }) }) .collect(); Self { assets } } /// Looks up an asset by its filename (e.g. `"style.css"`). pub fn get(&self, name: &str) -> Option<&StaticAsset> { self.assets.get(name) } /// Returns the versioned URL for an asset, e.g. `/static/style.css?v=`. pub fn url(&self, name: &str) -> Option { self.assets .get(name) .map(|asset| format!("/static/{name}?v={}", asset.hash)) } } fn content_hash(data: &[u8]) -> String { let mut hasher = Sha256::new(); hasher.update(data); hex::encode(hasher.finalize()) } /// Constructs a `StaticAssetStore` from `name => path` pairs, embedding each /// file's bytes at compile time via `include_bytes!`. macro_rules! static_assets { ($($name:literal => $path:literal),* $(,)?) => { StaticAssetStore::new(&[ $(($name, include_bytes!($path))),* ]) }; } /// The app's static assets, loaded once on first use. pub static STORE: LazyLock = LazyLock::new(|| { static_assets! { "style.css" => "../static/style.css", "passkey-login.js" => "../static/passkey-login.js", "passkey-register.js" => "../static/passkey-register.js", "password-toggle.js" => "../static/password-toggle.js", "rewards.js" => "../static/rewards.js", "htmx.min.js" => "../static/htmx.min.js", "idiomorph-ext.min.js" => "../static/idiomorph-ext.min.js", "htmx-ws.min.js" => "../static/htmx-ws.min.js", "favicon.ico" => "../static/favicon.ico", "favicon-32x32.png" => "../static/favicon-32x32.png", "apple-touch-icon.png" => "../static/apple-touch-icon.png", "logo.svg" => "../static/logo.svg", } }); /// Returns the versioned URL for a known asset, panicking if the name isn't /// registered (a programmer error, since these are compile-time constants). pub fn url(name: &str) -> String { STORE .url(name) .unwrap_or_else(|| panic!("unknown static asset: {name}")) }