Repository navigation
site(covers): cover-spec gate for posts dated from 2026-11-17 + build-time dark cover variants switched by theme - #347
Merged
objectstack-fleet[bot] merged 1 commit intoNov 2, 2026
Conversation
Decisions 3 (gate) and 4 of the #340 visual-style ruling. - scripts/cover-spec.mjs runs in `pnpm build`. Every cover*.svg of a post dated on or after SPEC_FROM (2026-11-17) is held to: at most 2 font sizes, title at least 56px, nothing under 28px, at most 8 text runs, at most 6 fill colours. Existing covers are grandfathered by the post's date. SPEC_FROM is the day after the last queued slot (11-16), because the publish queue re-dates its six pre-spec posts to their publish days. A queued post that slips past it takes a dated, named LATE_QUEUE entry; the list is empty, and a stale entry fails. --report prints one row per cover and never fails (88 of 88 break the spec today). - scripts/cover-dark.mjs writes cover*.dark.svg (gitignored) beside each light SVG cover, before `astro build` and in `pnpm dev`. Site tokens map to their dark values. Every other colour goes through a luminance inversion that keeps WCAG contrast; the script checks this for all 1,670 texts. The two covers with a dark ground are skipped. - ArticleCard, the BlogPost hero, and the home and cluster cards render both covers lazily and hide one by :root[data-theme]. Only one is downloaded per theme. The dark variant takes no --cover-filter. - svg-text-fit skips *.dark.svg, exports its parse helpers, and carries computed fills in checkSvg. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
Contributor
Author
席位核对Verdict: ACCEPT. I reviewed this against GitHub rather than trusting the report. It implements decisions 3 (gate) and 4 (dark variants) of the ruling on #340, comment 6570301123. The maintainer named the gate, so the default-no rule for new gates does not apply.
Generated by Claude Code |
This was referenced Nov 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #343
Implements decisions 3 (the gate part) and 4 of the visual-style ruling on #340 (comment 6570301123, maintainer 2026-11-02). AGENTS.md is untouched: the written spec lands in #344.
What changed
1. Cover spec gate:
scripts/cover-spec.mjs(pnpm cover:spec, runs inpnpm buildright aftersvg-text-fit).cover*.svgof a post dated on or afterSPEC_FROM = 2026-11-17against five rules:sizes(at most 2 font sizes),title(the largest text is at least 56px),min(no text under 28px),runs(at most 8 text elements), andfills(at most 6 distinct fill colours, where a gradient counts each of its stops).svg-text-fit, so a size set by a class in a style block counts the way a browser applies it. Text that names no fill counts as black.date. There is no list to maintain.--reportprints one row per cover for every cover, whatever its date, and never fails.--report --verbosealso prints each finding. Passing file arguments checks those files whatever their date, so an author can test a cover before it has a slot.SPEC_FROMis 2026-11-17 and not the merge date. The publish queue re-dates each queued post to its publish day. Six queued posts carry pre-spec covers: content(blog): What Do AI Models Lack in the Enterprise? A World, Hands and a Fence #260 (11-04), content(blog): how do you import a spreadsheet from an old system? import mappings, dry runs, three non-match outcomes #283 (11-06), content(blog): views, dashboards and apps are projections of the ontology — the naming test, measured on app-todo (slot 2026-11-10) #263 (11-09), content(blog): can you prove who opened a customer record, including an AI agent? record-view auditing (slot 2026-10-26) #284 (11-11), content(blog): how to let anonymous visitors submit a form into a governed business app (slot 2026-10-22) #280 (11-13) and content(blog): what os verify proves in two stages on ObjectStack 17.7.0 (slot 2026-10-24) #281 (11-16). With an earlier date, each would fail the gate on the day it publishes. The last queued slot is 11-16, so the gate starts the day after. Nothing needs the gate before then, because no new posts are being written until the queue ends (maintainer: 「暂时只发不写新的」).LATE_QUEUE, written as{ slug, date, reason }, or moveSPEC_FROM. An entry counts only while the post carries exactly that date. An entry that matches no post at its date fails the gate, so the list cannot outlive the queue. The list ships empty.2. Dark variants:
scripts/cover-dark.mjs(pnpm cover:dark, runs inpnpm buildbeforeastro buildand inpnpm dev).cover*.dark.svgbeside each light SVG cover. The files are build output and gitignored, and stale ones are deleted on each run.scripts/lib/cover-colors.mjsand works in two layers:TOKEN_MAPcopies the site's light-to-dark token pairs from the two:rootblocks in Base.astro, plus white mapped to the dark--surface.Y' = K/(Y+0.05) - 0.05. This keeps every WCAG contrast ratio the same size with the order flipped. Hue and chroma are kept, and chroma is cut only as far as the sRGB gamut requires.fill,stroke,stop-colorandcolorin style blocks, in style attributes and as presentation attributes. It never rewrites inside a text node.flood-coloris left alone, so shadows stay shadows.self-hosted-ai-app-platform, 2 files) get no variant and keep the--cover-filter.3. Templates.
ArticleCard.astro, the BlogPost hero, and the card images inMarketingHome.astroandClusterPage.astro. They find the variant throughsrc/lib/cover-variant.ts, which globs the dark variants and keys them by the post's frontmatter cover path. That is the samepostCoverFileruleog-imagesuses.cover-lightandcover-dark. CSS hides the one that does not match:root[data-theme].filter: none: the site(style): phase-0 quick wins — font-stack order, dark CTA, dark cover dimming, prose measure, CJK title line-height, tabular numerals, hamburger at 1024 #341--cover-filterdimming is removed wherever a variant exists. The token stays for raster covers and the two dark-ground SVGs.svg-text-fitskips*.dark.svgfiles, so their boxes are not counted twice. Its parse helpers are now exported for the two new scripts, andcheckSvgcarries each text's and each shape's computed fill.Deviations and trade-offs
[data-theme]alone, not a separateprefers-color-schememedia query. The head script in Base.astro setsdata-themebefore first paint, from the saved choice or else fromprefers-color-scheme. The whole dark palette keys on that attribute. A second media-query rule would show a dark cover on a light page whenever that script cannot run.fetchpriority="high"is kept on the hero pair. SVG covers without a variant and raster covers keepeager.Verification (every gate below was run on the committed tree)
Gates
pnpm content:lintcontent lint passed (399 files, 46 glossary terms checked)pnpm check(verify lock, slot www-cov-343)pnpm build(verify lock)cover-spec: OK — 0 covers of posts dated on or after 2026-11-17, 88 grandfathered by date;cover-dark: OK — 86 dark variants … 2 covers already dark, skipped; 1670 texts keep their contrast; 105 share images rendered; content lint --dist passedpnpm seo:smokeGate passes on current main: see the build row. All 88 covers are grandfathered.
A synthetic new-dated cover fails. A temporary post
zz-synthetic-cover-test, dated 2026-11-20, had a 64px title and a 14px caption:(TEXT stands for the element name, spelled without brackets here.) The
LATE_QUEUEpath was checked on the same synthetic post:1 excused by LATE_QUEUE.1 stale LATE_QUEUE entry.The entry and the directory were then removed, and the gate went back to 0 checked and 88 grandfathered. A second synthetic file triggered the other four rules:
fills,title,runsandsizeseach fired with the element named.--report: 88 of 88 covers break the spec. Every cover breaksmin,sizes,fillsandruns, and 80 breaktitle. One row per cover (runs, sizes, title px, runs under 28px, fills, rules broken) is ready for the redo card. First rows:Dark-mode cover luminance. Chromium, 1440, dark theme. "Before" is reproduced on this build by forcing the light image with main's
brightness(.85). Contrast is the mean against the dark--bg#101713.Network log: one variant per theme. Chromium, 1440. Each page was scrolled top to bottom so every lazy image reached the viewport. The table counts finished requests for cover files.
The one extra light SVG on the blog index and the cluster page is the
self-hosted-ai-app-platformcover. It is already dark and has no variant. The computedfilteron every dark variant isnone.8 locales × light/dark × 1440/390 on home cards, the blog index and a post hero: 96 screenshots, horizontal overflow 0 on all 96, 0 broken covers, 0 covers shown in the wrong theme. I looked at the contact sheets and at native-size light/dark pairs of six covers, including the ones with dark panels and gradient bars.
Not in this PR
--reportoutput is that card's input.🤖 Generated with Claude Code
https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr