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:
@@ -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'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 "No. 3" & \
|
||||
<b>4</b> for O'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();
|
||||
|
||||
Reference in New Issue
Block a user