Card images: hierarchical setting, compact-card image, explicit card-mode override

This commit is contained in:
2026-09-20 00:13:00 +00:00
parent 4bccf6855e
commit b248241af0
11 changed files with 609 additions and 90 deletions
+2 -2
View File
@@ -34,11 +34,11 @@ markdown-it-py renderer (html passthrough + attrs, footnote, deflist, tasklists,
The shared page layout as an html5tagger `Template` with placeholders (`Title`, `Brand`, `Banner`, `Nav`, `Sidebar`, `Main`), nav rendering straight from the `Data.menu` tree (siblings sorted by `Node.order`; nav links to content-less labels point at their first child via `first_leaf`, the first published descendant with content), and page/404 rendering.
Content pages get SEO/social meta (description, canonical link, Open Graph + twitter card) from heuristics over the rendered article: the description is the first paragraph's text, the share image prefers a `{.hero}`-classed image, then the first raster `<img>`, then the first SVG; the first `<video>` yields `og:video`; URLs are made absolute with the site origin (`SITE_URL``https://<hostname>` from the CLI hostname argument; on localhost the request's own base URL is the fallback); `article:published/modified_time` come from `Node.created`/`modified`. Additionally `twitter:image` pins extension-less `/_f/{hash}` share images to the `.webp` variant — X only honors WebP via twitter:image (not og:image) and its scraper cannot be trusted to negotiate via Accept. The page title is injected as `# {title}` when the markdown has no h1 of its own, so it never appears twice (it always supplies `<title>` and nav labels).
Content pages get SEO/social meta (description, canonical link, Open Graph + twitter card) from heuristics over the rendered article: the description is the first paragraph's text; the card image is the node's own `Node.image` when one resolves (nearest ancestor, front page last — see docs/content-model.md), otherwise mined from the article, preferring a `{.hero}`-classed image, then the first raster `<img>`, then the first SVG; the first `<video>` yields `og:video`; URLs are made absolute with the site origin (`SITE_URL``https://<hostname>` from the CLI hostname argument; on localhost the request's own base URL is the fallback); `article:published/modified_time` come from `Node.created`/`modified`. Additionally `twitter:image` pins extension-less `/_f/{hash}` card images to the `.webp` variant — X only honors WebP via twitter:image (not og:image) and its scraper cannot be trusted to negotiate via Accept. `twitter:card` is `summary_large_image` when the image's probed store dimensions suit a large card (>= 600px wide, aspect between 1.4 and 2.5; dimensions are read from the `<hash>.webp` derivative via pyvips, cached per hash) and `summary` for small or portrait images — external or unprobeable images keep the presence-based default (large when an image exists). The node's `Node.large` setting overrides that pick per article (False = small, True = large, the default None = automatic; NOT inherited like `Node.image`). The page title is injected as `# {title}` when the markdown has no h1 of its own, so it never appears twice (it always supplies `<title>` and nav labels).
The navbar holds top-level items only; the current section's subitems go to a left `#sidebar` as a nested list (the section's direct children plain, deeper levels indented with article-list-style markers), rendered only from the second level down — main-level pages list their children as cards after the content instead. Below that, the sidebar renders when the section offers at least two published items, or exactly one while viewing anything other than that only page — the section index, a 404, a grandchild (so those pages can reach the child), and also on that only page itself when it has published children of its own; no aside element at all on the front page, main-level pages, leaf pages and the sole childless page of a one-page section. Also, category labels are nodes without content — None *or* empty markdown — and their nav links point at their first child page. Dynamic regions have stable ids (`#page-banner`, `#nav`, `#sidebar`, `#main`) for fetch-navigation swaps (`#sidebar` may be absent on either side of a swap).
Any page with published children — a category page — lists them as a card grid (`nav.cards`) after the markdown content, as does the content-less category 404. Each card links to the child page (a content-less child to its first leaf) and shows the child's share image (the same hero → first raster → first SVG heuristics as `og:image`) as a full-card cover with the title overlaid.
Any page with published children — a category page — lists them as a card grid (`nav.cards`) after the markdown content, as does the content-less category 404. Each card links to the child page (a content-less child to its first leaf) and shows the child's card image (its resolved `Node.image` when set, else the same hero → first raster → first SVG heuristics as `og:image`). The layout follows the same selection as `twitter:card` (_card_large — the child's per-article `Node.large` override, else the image's probed dimensions): large cards show the image as a full-card cover with the title overlaid, small cards (`.card.compact`) split horizontally at the golden ratio (two sub-grids, top φ : bottom 1): the square image fills the top part with the title beside it at its bottom, the article description (which only the small format carries) tops the bottom part — title and description carry the translucent band (the same band color as the large cards' title) as their own background; imageless cards keep the image space as a blank gradient. Each card carries the target article's language as its `lang` (the page language when the target is translated into it, else the target's primary language — matching the per-card text fallback) so the clamped title/description hyphenate correctly (`hyphens: auto`).
## `seed.py`