Remove everything that can change after first paint

Firefox users saw a flash on the first navigation after an idle spell, and
Chrome occasionally flashed white. Nothing in the network path explained it
(assets are immutable, the stylesheet is render-blocking, a cold Firefox
load paints fully styled), so this removes every remaining way a page could
look different between its first paint and its final state:

- Both Newsreader faces are embedded in the served stylesheet as data: URIs
  (CSP gains `font-src 'self' data:`; the preloads go away). A font fetched
  by URL is applied after first paint whenever the browser has to bring it
  back from disk, which is exactly the "first click after a while" case.
- The cross-document view transition is gone; the operator wants snappy.
- A color-scheme meta, kept in step with the saved theme, so the canvas the
  browser paints before the stylesheet is the right shade.
- The theme toggle's icon and label are chosen by CSS from html[data-theme]
  (set pre-paint by theme.js) instead of being rewritten by app.js.
- Dashboard table filters are rendered by the templates (shown under
  `.has-js`) rather than inserted by app.js, so tables no longer jump.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MD4VWGq6mGcd8Bg67qyx9k
This commit is contained in:
2026-09-04 18:37:44 +00:00
co-authored by Claude Fable 5.1
parent 9acd1b0800
commit 1047cf4633
13 changed files with 91 additions and 82 deletions
+43 -15
View File
@@ -211,9 +211,38 @@ pub struct Page {
}
/// First 12 hex digits of the SHA-256 over the embedded CSS and JS assets.
const NEWSREADER: &[u8] = include_bytes!("static/fonts/Newsreader.woff2");
const NEWSREADER_ITALIC: &[u8] = include_bytes!("static/fonts/Newsreader-italic.woff2");
/// The stylesheet with both Newsreader faces embedded as `data:` URIs.
///
/// A font referenced by URL is applied *after* first paint whenever the browser
/// has to bring it back from the disk cache (Firefox drops fonts from memory as
/// soon as no page uses them), which shows as a flash of invisible or fallback
/// text on the first navigation after an idle spell. Fonts embedded in the
/// render-blocking stylesheet are decoded synchronously, so the first paint is
/// already set in Newsreader.
pub static APP_CSS: LazyLock<String> = LazyLock::new(|| {
use base64::Engine;
let data_uri = |bytes: &[u8]| {
format!(
"url(data:font/woff2;base64,{})",
base64::engine::general_purpose::STANDARD.encode(bytes)
)
};
include_str!("static/app.css")
.replace("url(/static/Newsreader.woff2)", &data_uri(NEWSREADER))
.replace(
"url(/static/Newsreader-italic.woff2)",
&data_uri(NEWSREADER_ITALIC),
)
});
pub static ASSET_VERSION: LazyLock<String> = LazyLock::new(|| {
let mut hasher = Sha256::new();
hasher.update(include_str!("static/app.css"));
hasher.update(NEWSREADER);
hasher.update(NEWSREADER_ITALIC);
hasher.update(include_str!("static/app.js"));
hasher.update(include_str!("static/theme.js"));
hex::encode(hasher.finalize())[..12].to_string()
@@ -411,7 +440,7 @@ pub async fn security_headers(request: Request, next: Next) -> Response {
headers.insert(
header::HeaderName::from_static("content-security-policy"),
HeaderValue::from_static(
"default-src 'self'; img-src * data:; style-src 'self'; script-src 'self'; frame-ancestors 'none'; form-action 'self'",
"default-src 'self'; img-src * data:; font-src 'self' data:; style-src 'self'; script-src 'self'; frame-ancestors 'none'; form-action 'self'",
),
);
headers.insert(
@@ -537,10 +566,7 @@ async fn static_asset(
headers: axum::http::HeaderMap,
) -> Response {
let asset: (&str, &'static [u8]) = match file.as_str() {
"app.css" => (
"text/css; charset=utf-8",
include_str!("static/app.css").as_bytes(),
),
"app.css" => ("text/css; charset=utf-8", APP_CSS.as_bytes()),
"app.js" => (
"application/javascript; charset=utf-8",
include_str!("static/app.js").as_bytes(),
@@ -557,14 +583,8 @@ async fn static_asset(
"image/svg+xml",
include_str!("static/favicon.svg").as_bytes(),
),
"Newsreader.woff2" => (
"font/woff2",
include_bytes!("static/fonts/Newsreader.woff2"),
),
"Newsreader-italic.woff2" => (
"font/woff2",
include_bytes!("static/fonts/Newsreader-italic.woff2"),
),
"Newsreader.woff2" => ("font/woff2", NEWSREADER),
"Newsreader-italic.woff2" => ("font/woff2", NEWSREADER_ITALIC),
_ => return WebError::NotFound.into_response(),
};
let etag = format!("\"{}\"", hex::encode(Sha256::digest(asset.1)));
@@ -1080,8 +1100,16 @@ mod tests {
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)));
assert!(html.contains("rel=\"preload\" href=\"/static/Newsreader.woff2\""));
assert!(html.contains("rel=\"preload\" href=\"/static/Newsreader-italic.woff2\""));
// The fonts ride inside the stylesheet; a preload would fetch them twice.
assert!(!html.contains("rel=\"preload\""), "{html}");
}
#[test]
fn stylesheet_embeds_both_newsreader_faces() {
let css = APP_CSS.as_str();
assert_eq!(css.matches("url(data:font/woff2;base64,").count(), 2);
assert!(!css.contains("/static/Newsreader"));
assert!(css.contains("font-display:block"), "{}", &css[..200]);
}
#[tokio::test]