cache-bust css via content-hash ?v; keycap button style

This commit is contained in:
Jean Chevronnet 2026-08-31 01:35:59 +00:00
parent c3c9e08a9e
commit 55c6685df1
No known key found for this signature in database
GPG key ID: 439666D63A9477E4
4 changed files with 47 additions and 14 deletions

View file

@ -15,8 +15,27 @@ use axum::{
Router, Router,
}; };
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::OnceLock;
use tower_http::{compression::CompressionLayer, trace::TraceLayer}; use tower_http::{compression::CompressionLayer, trace::TraceLayer};
/// Short content hash of the bundled CSS, used as a `?v=` cache-buster: a changed
/// stylesheet gets a new URL (fetched fresh), an unchanged one stays cached.
fn asset_ver() -> &'static str {
static V: OnceLock<String> = OnceLock::new();
V.get_or_init(|| {
let mut h: u64 = 0xcbf29ce484222325;
for b in include_str!("../static/style.css")
.bytes()
.chain(include_str!("../static/docs.css").bytes())
{
h ^= b as u64;
h = h.wrapping_mul(0x100000001b3);
}
format!("{h:x}")
})
.as_str()
}
/// Display-ready snapshot of the live network state for templates. /// Display-ready snapshot of the live network state for templates.
struct Status { struct Status {
online: bool, online: bool,
@ -48,6 +67,7 @@ struct AppState {
#[template(path = "index.html")] #[template(path = "index.html")]
struct IndexTemplate { struct IndexTemplate {
active: &'static str, active: &'static str,
css_v: &'static str,
status: Status, status: Status,
commits: Vec<commits::Commit>, commits: Vec<commits::Commit>,
} }
@ -56,18 +76,21 @@ struct IndexTemplate {
#[template(path = "features.html")] #[template(path = "features.html")]
struct FeaturesTemplate { struct FeaturesTemplate {
active: &'static str, active: &'static str,
css_v: &'static str,
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "connect.html")] #[template(path = "connect.html")]
struct ConnectTemplate { struct ConnectTemplate {
active: &'static str, active: &'static str,
css_v: &'static str,
} }
#[derive(Template)] #[derive(Template)]
#[template(path = "services.html")] #[template(path = "services.html")]
struct ServicesTemplate { struct ServicesTemplate {
active: &'static str, active: &'static str,
css_v: &'static str,
} }
struct SidePage { struct SidePage {
@ -84,6 +107,7 @@ struct SideSection {
#[template(path = "docs.html")] #[template(path = "docs.html")]
struct DocsTemplate { struct DocsTemplate {
active: &'static str, active: &'static str,
css_v: &'static str,
page_title: &'static str, page_title: &'static str,
body: &'static str, body: &'static str,
toc: &'static [docs::Heading], toc: &'static [docs::Heading],
@ -104,16 +128,16 @@ fn page<T: Template>(t: T) -> Response {
async fn index(State(st): State<AppState>) -> Response { async fn index(State(st): State<AppState>) -> Response {
let commits = st.commits.read().ok().map(|g| g.clone()).unwrap_or_default(); let commits = st.commits.read().ok().map(|g| g.clone()).unwrap_or_default();
page(IndexTemplate { active: "home", status: view(&st.stats), commits }) page(IndexTemplate { active: "home", css_v: asset_ver(), status: view(&st.stats), commits })
} }
async fn features() -> Response { async fn features() -> Response {
page(FeaturesTemplate { active: "features" }) page(FeaturesTemplate { active: "features", css_v: asset_ver() })
} }
async fn connect() -> Response { async fn connect() -> Response {
page(ConnectTemplate { active: "connect" }) page(ConnectTemplate { active: "connect", css_v: asset_ver() })
} }
async fn services() -> Response { async fn services() -> Response {
page(ServicesTemplate { active: "services" }) page(ServicesTemplate { active: "services", css_v: asset_ver() })
} }
async fn docs_index() -> Response { async fn docs_index() -> Response {
@ -137,6 +161,7 @@ async fn docs_page(Path(slug): Path<String>) -> Response {
let (prev, next) = docs::neighbours(&slug); let (prev, next) = docs::neighbours(&slug);
page(DocsTemplate { page(DocsTemplate {
active: "docs", active: "docs",
css_v: asset_ver(),
page_title: p.title, page_title: p.title,
body: &p.html, body: &p.html,
toc: &p.toc, toc: &p.toc,
@ -154,13 +179,19 @@ async fn docs_search() -> impl IntoResponse {
async fn style() -> impl IntoResponse { async fn style() -> impl IntoResponse {
( (
[(header::CONTENT_TYPE, "text/css; charset=utf-8")], [
(header::CONTENT_TYPE, "text/css; charset=utf-8"),
(header::CACHE_CONTROL, "public, max-age=31536000, immutable"),
],
include_str!("../static/style.css"), include_str!("../static/style.css"),
) )
} }
async fn docs_css() -> impl IntoResponse { async fn docs_css() -> impl IntoResponse {
( (
[(header::CONTENT_TYPE, "text/css; charset=utf-8")], [
(header::CONTENT_TYPE, "text/css; charset=utf-8"),
(header::CACHE_CONTROL, "public, max-age=31536000, immutable"),
],
include_str!("../static/docs.css"), include_str!("../static/docs.css"),
) )
} }

View file

@ -1,6 +1,6 @@
/* echoIRCd — teal is echo's own colour. Light by default; dark via [data-theme]. */ /* echoIRCd — teal is echo's own colour. Light by default; dark via [data-theme]. */
:root{ :root{
--teal:#12a594; --teal-d:#0e8577; --teal-dd:#0a6a5f; --teal-t:#e7f6f3; --teal:#12a594; --teal-d:#0e8577; --teal-dd:#0a6a5f; --teal-t:#e7f6f3; --teal-edge:#0a6a5f;
--ink:#1b2733; --muted:#5c6773; --faint:#8b95a0; --ink:#1b2733; --muted:#5c6773; --faint:#8b95a0;
--line:#e4e8eb; --line2:#d8dee2; --bg:#f5f6f7; --card:#ffffff; --panel:#fbfcfd; --line:#e4e8eb; --line2:#d8dee2; --bg:#f5f6f7; --card:#ffffff; --panel:#fbfcfd;
--code-bg:#132430; --code-fg:#e7edf0; --cmd:#6fe0cf; --code-bg:#132430; --code-fg:#e7edf0; --cmd:#6fe0cf;
@ -12,7 +12,7 @@
--wrap:1080px; --wrap:1080px;
} }
:root[data-theme="dark"]{ :root[data-theme="dark"]{
--teal:#15b6a3; --teal-d:#48d8c4; --teal-dd:#7de9db; --teal-t:#0f2d29; --teal:#15b6a3; --teal-d:#48d8c4; --teal-dd:#7de9db; --teal-t:#0f2d29; --teal-edge:#0b8474;
--ink:#e3ece8; --muted:#93a8a1; --faint:#66807a; --ink:#e3ece8; --muted:#93a8a1; --faint:#66807a;
--line:#1b2e29; --line2:#26403a; --bg:#0b1614; --card:#132622; --panel:#0f1e1b; --line:#1b2e29; --line2:#26403a; --bg:#0b1614; --card:#132622; --panel:#0f1e1b;
--purple-line:#4f5a7a; --purple-fg:#bfc7ee; --purple-bg:#182034; --bq-fg:#8fe6d8; --purple-line:#4f5a7a; --purple-fg:#bfc7ee; --purple-bg:#182034; --bq-fg:#8fe6d8;
@ -36,12 +36,14 @@ code{font-family:var(--mono);font-size:.9em;background:rgba(18,165,148,.09);colo
.mono-line{font-family:var(--mono);color:var(--muted)} .mono-line{font-family:var(--mono);color:var(--muted)}
/* buttons */ /* buttons */
.btn{display:inline-block;font-weight:600;font-size:.95rem;padding:.6rem 1.25rem;border-radius:6px;border:1.5px solid transparent;transition:background .14s ease,border-color .14s ease,color .14s ease} .btn{display:inline-flex;align-items:center;gap:.45rem;font-weight:600;font-size:.95rem;letter-spacing:.005em;padding:.58rem 1.25rem;border-radius:5px;border:1.5px solid transparent;cursor:pointer;transition:background .14s ease,border-color .14s ease,color .14s ease,box-shadow .09s ease,transform .09s ease}
.btn:hover{text-decoration:none} .btn:hover{text-decoration:none}
.btn-primary{background:var(--teal);border-color:var(--teal);color:#fff} .btn-primary{background:var(--teal);border-color:var(--teal);color:#fff;box-shadow:0 3px 0 var(--teal-edge),inset 0 1px 0 rgba(255,255,255,.2)}
.btn-primary:hover{background:var(--teal-d);border-color:var(--teal-d)} .btn-primary:hover{background:var(--teal-d);border-color:var(--teal-d)}
.btn-ghost{border-color:var(--line2);color:var(--ink);background:transparent} .btn-primary:active{transform:translateY(2px);box-shadow:0 1px 0 var(--teal-edge),inset 0 1px 0 rgba(255,255,255,.15)}
.btn-ghost:hover{border-color:var(--teal);color:var(--teal-d);background:var(--teal-t)} .btn-ghost{border-color:var(--teal);color:var(--teal-d);background:transparent}
.btn-ghost:hover{background:var(--teal-t);color:var(--teal-dd)}
.btn-ghost:active{transform:translateY(1px)}
/* top bar (teal in both themes) */ /* top bar (teal in both themes) */
.topbar{background:var(--teal);color:#fff;position:sticky;top:0;z-index:20;box-shadow:0 2px 12px -6px rgba(0,0,0,.35)} .topbar{background:var(--teal);color:#fff;position:sticky;top:0;z-index:20;box-shadow:0 2px 12px -6px rgba(0,0,0,.35)}

View file

@ -14,7 +14,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css?v={{ css_v }}">
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>

View file

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}{{ page_title }} — echoIRCd Docs{% endblock %} {% block title %}{{ page_title }} — echoIRCd Docs{% endblock %}
{% block head %} {% block head %}
<link rel="stylesheet" href="/static/docs.css"> <link rel="stylesheet" href="/static/docs.css?v={{ css_v }}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css">
{% endblock %} {% endblock %}
{% block content %} {% block content %}