Fix the two Lighthouse findings: muted contrast and meta description

Accessibility (95 -> 100). Light `--muted` #7a7568 was 4.14:1 on `--paper`
and 3.72:1 on `--paper-2`, under the 4.5:1 AA floor for normal text — and
muted is the ears line, the nav, bylines, the footer, table headers and
placeholders, so it was most of the page's small type. An audit of every
text token against both surfaces turned up one more: light `--warn`
#8a6d1f at 4.42 / 3.97.

Both are darkened just enough to clear 4.5 on the darker of the two
surfaces, at constant hue and saturation, so the warm grey and the ochre
read the same:

  --muted  #7a7568 -> #6b665a   4.14/3.72 -> 5.16/4.63
  --warn   #8a6d1f -> #7d631c   4.42/3.97 -> 5.16/4.63

Everything else already passed on both surfaces (light ink 15.54/13.96,
ink-2 7.83/7.03, accent 6.73/6.04, loved 5.44/4.89, good 5.29/4.76, down
5.96/5.36), as did the whole dark set against #151513 / #1e1d1a (muted
5.31/4.90 is its floor), so the dark blocks are untouched and stay
identical to each other. `.badge` tints sit on near-paper, so the new
token values carry it.

SEO (91 -> 100). The layout had no `<meta name="description">`. `Page`
now carries one — `DEFAULT_DESCRIPTION` for the site, so all ~26
`Page::new` callers keep their signature — with `with_description` for
the pages worth writing one for: the issue page (issue number, date and
the counts the masthead already prints), the archive, the empty-latest
landing page and sign-in. Dashboard pages keep the default.

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 19:43:56 +00:00
co-authored by Claude Fable 5.1
parent 239125a8e5
commit 04bf14f710
6 changed files with 105 additions and 9 deletions
+58
View File
@@ -197,9 +197,19 @@ pub struct Flash {
pub text: String,
}
/// The site-wide `<meta name="description">`, used by every page that does not
/// set one of its own. Search engines truncate around 160 characters.
pub const DEFAULT_DESCRIPTION: &str = concat!(
"A daily newspaper of the web: articles hand-picked from one reader's feeds, ",
"published every morning as an EPUB and readable here."
);
#[derive(Debug, Clone)]
pub struct Page {
pub title: String,
/// The `<meta name="description">` for this page; `DEFAULT_DESCRIPTION`
/// unless a handler overrides it with [`Page::with_description`].
pub description: String,
pub viewer: Option<Viewer>,
pub flash: Option<Flash>,
pub active_nav: String,
@@ -252,6 +262,7 @@ impl Page {
pub fn new(title: impl Into<String>, viewer: Option<Viewer>, active_nav: &str) -> Self {
Self {
title: title.into(),
description: DEFAULT_DESCRIPTION.to_string(),
viewer,
flash: None,
active_nav: active_nav.to_string(),
@@ -260,6 +271,13 @@ impl Page {
}
}
/// Replace the site-wide description with one written for this page.
#[must_use]
pub fn with_description(mut self, text: impl Into<String>) -> Self {
self.description = text.into();
self
}
pub fn is_admin(&self) -> bool {
self.viewer
.as_ref()
@@ -1104,6 +1122,46 @@ mod tests {
assert!(!html.contains("rel=\"preload\""), "{html}");
}
#[test]
fn pages_render_a_meta_description_and_escape_it() {
let render = |page: Page| {
ErrorTemplate {
page,
heading: "h".into(),
message: "m".into(),
}
.render()
.unwrap()
};
// Every page carries a description; the default one when none is set.
assert!(DEFAULT_DESCRIPTION.len() <= 160, "{DEFAULT_DESCRIPTION}");
let html = render(Page::new("t", None, "latest"));
assert!(
html.contains(
"<meta name=\"description\" content=\"A daily newspaper of the web: articles \
hand-picked from one reader&#39;s feeds, published every morning as an EPUB and \
readable here.\">"
),
"{html}"
);
// A page-specific one replaces it, HTML-escaped into the attribute.
let page = Page::new("t", None, "latest")
.with_description("Issue \"No. 3\" & <b>4</b> for O'Donnell");
assert_eq!(page.description, "Issue \"No. 3\" & <b>4</b> for O'Donnell");
let html = render(page);
assert!(
html.contains(
"<meta name=\"description\" content=\"Issue &#34;No. 3&#34; &#38; \
&#60;b&#62;4&#60;/b&#62; for O&#39;Donnell\">"
),
"{html}"
);
assert!(!html.contains("<b>4</b>"), "{html}");
assert!(!html.contains(DEFAULT_DESCRIPTION), "{html}");
}
#[test]
fn stylesheet_embeds_both_newsreader_faces() {
let css = APP_CSS.as_str();