Skip to main content

codoseo_web/
assets.rs

1//! Static assets embedded in the binary (the self-hosted UI makes no outside requests).
2//!
3//! Each asset is served under a content-hashed name (`/assets/app.3f2a1b9c.css`) with a
4//! one-year immutable cache, so a deploy never serves a stale stylesheet. CSS, JS and SVG are
5//! gzip-compressed once, the first time any asset is asked for.
6
7use std::borrow::Cow;
8use std::io::Write;
9use std::sync::LazyLock;
10
11use axum::extract::Path;
12use axum::http::{HeaderMap, HeaderValue, StatusCode, header};
13use axum::response::{IntoResponse, Response};
14use flate2::Compression;
15use flate2::write::GzEncoder;
16use xxhash_rust::xxh3::xxh3_64;
17
18struct Source {
19    name: &'static str,
20    content_type: &'static str,
21    bytes: &'static [u8],
22}
23
24const SOURCES: &[Source] = &[
25    Source {
26        name: "Geist-Variable.woff2",
27        content_type: "font/woff2",
28        bytes: include_bytes!("../assets/fonts/Geist-Variable.woff2"),
29    },
30    Source {
31        name: "GeistMono-Variable.woff2",
32        content_type: "font/woff2",
33        bytes: include_bytes!("../assets/fonts/GeistMono-Variable.woff2"),
34    },
35    Source {
36        name: "htmx.min.js",
37        content_type: "text/javascript; charset=utf-8",
38        bytes: include_bytes!("../assets/htmx.min.js"),
39    },
40    Source {
41        name: "app.js",
42        content_type: "text/javascript; charset=utf-8",
43        bytes: include_bytes!("../assets/app.js"),
44    },
45    Source {
46        name: "favicon.svg",
47        content_type: "image/svg+xml",
48        bytes: include_bytes!("../assets/favicon.svg"),
49    },
50    Source {
51        name: "landing.css",
52        content_type: "text/css; charset=utf-8",
53        bytes: include_bytes!("../assets/landing.css"),
54    },
55    // Last, so its font URLs can be rewritten to the fonts' hashed names.
56    Source {
57        name: "app.css",
58        content_type: "text/css; charset=utf-8",
59        bytes: include_bytes!("../assets/app.css"),
60    },
61];
62
63pub struct Asset {
64    pub name: &'static str,
65    /// `/assets/<stem>.<hash>.<ext>`
66    pub url: String,
67    content_type: &'static str,
68    bytes: Cow<'static, [u8]>,
69    gzip: Option<Vec<u8>>,
70}
71
72pub struct Assets {
73    items: Vec<Asset>,
74}
75
76static ASSETS: LazyLock<Assets> = LazyLock::new(Assets::build);
77
78impl Assets {
79    fn build() -> Assets {
80        let mut items: Vec<Asset> = Vec::with_capacity(SOURCES.len());
81        for src in SOURCES {
82            let mut bytes = Cow::Borrowed(src.bytes);
83            if src.name.ends_with(".css") {
84                // `url(/assets/Geist-Variable.woff2)` in the stylesheet -> the hashed URL.
85                let mut text = String::from_utf8_lossy(src.bytes).into_owned();
86                for done in &items {
87                    text = text.replace(&format!("/assets/{}", done.name), &done.url);
88                }
89                bytes = Cow::Owned(text.into_bytes());
90            }
91            let hash = xxh3_64(&bytes);
92            let (stem, ext) = src.name.rsplit_once('.').unwrap_or((src.name, ""));
93            let url = format!("/assets/{stem}.{:08x}.{ext}", hash as u32);
94            let gzip = compressible(src.content_type).then(|| gzip(&bytes));
95            items.push(Asset {
96                name: src.name,
97                url,
98                content_type: src.content_type,
99                bytes,
100                gzip,
101            });
102        }
103        Assets { items }
104    }
105
106    fn by_url(&self, url: &str) -> Option<&Asset> {
107        self.items.iter().find(|a| a.url == url)
108    }
109
110    fn by_name(&self, name: &str) -> Option<&Asset> {
111        self.items.iter().find(|a| a.name == name)
112    }
113}
114
115fn compressible(content_type: &str) -> bool {
116    content_type.starts_with("text/") || content_type.starts_with("image/svg")
117}
118
119fn gzip(bytes: &[u8]) -> Vec<u8> {
120    let mut enc = GzEncoder::new(Vec::new(), Compression::best());
121    enc.write_all(bytes).expect("writing to a Vec cannot fail");
122    enc.finish().expect("writing to a Vec cannot fail")
123}
124
125/// The hashed URL of an embedded asset, for templates: `{{ crate::assets::url("app.css") }}`.
126pub fn url(name: &str) -> &'static str {
127    match ASSETS.by_name(name) {
128        Some(a) => a.url.as_str(),
129        None => panic!("unknown asset {name:?}"),
130    }
131}
132
133/// `GET /assets/{file}`
134pub async fn serve(Path(file): Path<String>, headers: HeaderMap) -> Response {
135    let Some(asset) = ASSETS.by_url(&format!("/assets/{file}")) else {
136        return (
137            StatusCode::NOT_FOUND,
138            [(header::CACHE_CONTROL, "no-store")],
139            "not found",
140        )
141            .into_response();
142    };
143    let accepts_gzip = headers
144        .get(header::ACCEPT_ENCODING)
145        .and_then(|v| v.to_str().ok())
146        .is_some_and(|v| v.split(',').any(|e| e.trim().starts_with("gzip")));
147
148    let mut res = match (&asset.gzip, accepts_gzip) {
149        (Some(gz), true) => {
150            let mut r = gz.clone().into_response();
151            r.headers_mut()
152                .insert(header::CONTENT_ENCODING, HeaderValue::from_static("gzip"));
153            r
154        }
155        _ => asset.bytes.to_vec().into_response(),
156    };
157    let h = res.headers_mut();
158    h.insert(
159        header::CONTENT_TYPE,
160        HeaderValue::from_static(asset.content_type),
161    );
162    h.insert(
163        header::CACHE_CONTROL,
164        HeaderValue::from_static("public, max-age=31536000, immutable"),
165    );
166    if asset.gzip.is_some() {
167        h.insert(header::VARY, HeaderValue::from_static("accept-encoding"));
168    }
169    res
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn every_asset_has_a_hashed_url() {
178        for src in SOURCES {
179            let u = url(src.name);
180            assert!(u.starts_with("/assets/"), "{u}");
181            assert_ne!(u, format!("/assets/{}", src.name));
182        }
183    }
184
185    #[test]
186    fn css_points_at_hashed_fonts() {
187        let css = ASSETS.by_name("app.css").unwrap();
188        let text = std::str::from_utf8(&css.bytes).unwrap();
189        assert!(text.contains(url("Geist-Variable.woff2")));
190        assert!(!text.contains("/assets/Geist-Variable.woff2"));
191    }
192}