Static assets: bust caches with a content hash instead of the crate version
/static/app.css, app.js and theme.js URLs now carry the first 12 hex digits of a SHA-256 over the embedded assets, so any CSS or JS change reaches browsers that cached the previous build without a version bump. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHyYupFdBiR4VfoUM7NjSM
This commit is contained in:
+33
-1
@@ -6,7 +6,7 @@ pub mod session;
|
||||
pub mod users;
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
use std::time::SystemTime;
|
||||
|
||||
use askama::Template;
|
||||
@@ -204,8 +204,21 @@ pub struct Page {
|
||||
pub flash: Option<Flash>,
|
||||
pub active_nav: String,
|
||||
pub version: &'static str,
|
||||
/// Cache-busting token for `/static/*.css|js` URLs: a content hash, so
|
||||
/// any stylesheet or script change reaches browsers that cached the
|
||||
/// previous build (they are served with a one-day `max-age`).
|
||||
pub asset_version: &'static str,
|
||||
}
|
||||
|
||||
/// First 12 hex digits of the SHA-256 over the embedded CSS and JS assets.
|
||||
pub static ASSET_VERSION: LazyLock<String> = LazyLock::new(|| {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(include_str!("static/app.css"));
|
||||
hasher.update(include_str!("static/app.js"));
|
||||
hasher.update(include_str!("static/theme.js"));
|
||||
hex::encode(hasher.finalize())[..12].to_string()
|
||||
});
|
||||
|
||||
impl Page {
|
||||
pub fn new(title: impl Into<String>, viewer: Option<Viewer>, active_nav: &str) -> Self {
|
||||
Self {
|
||||
@@ -214,6 +227,7 @@ impl Page {
|
||||
flash: None,
|
||||
active_nav: active_nav.to_string(),
|
||||
version: crate::VERSION,
|
||||
asset_version: ASSET_VERSION.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -995,6 +1009,24 @@ mod tests {
|
||||
assert_eq!(session.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn asset_urls_carry_a_content_hash_not_the_crate_version() {
|
||||
let page = Page::new("t", None, "latest");
|
||||
assert_eq!(page.asset_version.len(), 12);
|
||||
assert!(page.asset_version.chars().all(|c| c.is_ascii_hexdigit()));
|
||||
assert_ne!(page.asset_version, crate::VERSION);
|
||||
let html = ErrorTemplate {
|
||||
page,
|
||||
heading: "h".into(),
|
||||
message: "m".into(),
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
let expected = format!("/static/app.css?v={}", ASSET_VERSION.as_str());
|
||||
assert!(html.contains(&expected), "{html}");
|
||||
assert!(!html.contains(&format!("/static/app.css?v={}", crate::VERSION)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn static_assets_use_content_hash_etags() {
|
||||
let (_dir, state) = test_state(Config::default()).await;
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ page.title }} · The Daily EPUB</title>
|
||||
<script src="/static/theme.js?v={{ page.version }}"></script>
|
||||
<script src="/static/theme.js?v={{ page.asset_version }}"></script>
|
||||
<link rel="preload" href="/static/Newsreader.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/static/app.css?v={{ page.version }}">
|
||||
<link rel="stylesheet" href="/static/app.css?v={{ page.asset_version }}">
|
||||
<link rel="alternate" type="application/atom+xml" title="The Daily EPUB" href="/feed.xml">
|
||||
<link rel="icon" href="/static/favicon.svg">
|
||||
</head>
|
||||
@@ -47,6 +47,6 @@
|
||||
{% match page.flash %}{% when Some with (flash) %}<div class="mx-auto max-w-7xl px-4 sm:px-6"><div class="flash {{ flash.kind }}" role="status">{{ flash.text }}</div></div>{% when None %}{% endmatch %}
|
||||
<main id="content" class="min-h-[68vh]">{% block content %}{% endblock %}</main>
|
||||
<footer class="mx-auto mt-16 flex max-w-7xl flex-wrap justify-between gap-2 border-t border-rule px-4 py-6 font-sans text-[0.72rem] uppercase tracking-[0.1em] text-muted sm:px-6"><span>The Daily EPUB</span><span>daily-epub {{ page.version }}</span></footer>
|
||||
<script src="/static/app.js?v={{ page.version }}"></script>
|
||||
<script src="/static/app.js?v={{ page.asset_version }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user