Skip to content

fix(website): ATC theme follow-ups — favicon, social cards, whitepapers - #1059

Merged
blove merged 6 commits into
mainfrom
blove/atc-theme-followups
Sep 8, 2026
Merged

fix(website): ATC theme follow-ups — favicon, social cards, whitepapers#1059
blove merged 6 commits into
mainfrom
blove/atc-theme-followups

Conversation

@blove

@blove blove commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

The follow-ups deferred from #1058, plus two content bugs found while doing them. Everything here is downstream of the retheme now live on threadplane.ai.

The brand reaches the artifacts people actually take away

  • Favicon — was still a navy field with a white plane. Now the yellow squircle with an ink glyph, matching the nav mark exactly.
  • Social cards — the Satori renderer bundles its own TTFs, so its colours had followed the retheme while its type had not: shared links previewed in EB Garamond against an Archivo Black site. Now Archivo Black + Archivo, with the build script handling a static face (Archivo Black has no fvar) and pinning Archivo's wdth axis. Verified by rendering /opengraph-image and reading the PNG back, because the specs only assert the files exist.
  • Whitepapers — the document a lead downloads from the lead form was entirely old-brand. Rethemed and regenerated, both HTML and PDFs.

Two content bugs in the whitepapers, found while rethemeing them

Neither is cosmetic, and both were already shipping:

  1. mdToHTML had no inline-code rule and never escaped markup. Every `<chat-message-list>` in the prose reached the reader as an empty pair of backticks — the browser parsed the name as an unknown element, which renders as nothing. The chat paper was missing twelve component names, producing sentences like " manages scroll position, virtualization hints, and message grouping logic."

  2. Paragraphs were split after fenced blocks were built, so any code sample containing a blank line was torn in half and its second half wrapped in a <p>, leaving the sample's own markup unescaped. The chat-debug example lost both of its Angular element lines that way.

The source text survived in the committed HTML, so both are repaired on the way back through --rerender rather than left in a published document with words missing. mdToHTML is fixed too — fenced blocks are lifted to placeholders before the split and escaped exactly once — so a fresh generation cannot reintroduce either.

Smaller items

  • --form-focus-ring had been retuned to the signal yellow, which composites to 1.19:1 on white — softer than the navy it replaced, so the halo had visibly thinned. Mixed at 55% it reads again while staying the signal colour. (The focused control's indicator is its navy border at 15.37:1; this is the halo around it.)
  • .hero-trust was fully overridden by .hero-strip and did nothing.
  • PlaneMark's docblock claimed a navy field and an inherited colour — both false.
  • --color-alert is recorded as reserved with no consumer, so it doesn't read as an oversight.

Test Plan

  • apps/website — 1375/1375
  • libs/design-tokens — 99/99
  • npx nx build website
  • Playwright e2e — full suite green
  • Social card rendered and read back: HTTP 200, image/png, headline unmistakably Archivo Black, no Noto fallback
  • Whitepaper PDFs rendered to PNG and read: Archivo Black headings with genuine letterforms (not synthesized), Archivo body, true italic, navy emphasis, one restrained yellow rule per page opener
  • Zero broken-fence artifacts and zero raw unknown elements across all four papers

Note

npm run generate-whitepaper cannot reach the model — the ANTHROPIC_API_KEY in .env reports its credit balance is too low. The --rerender path added here re-skins the committed prose with no model calls, which is the right tool for a design change anyway. Fresh chapters need the account topped up.

🤖 Generated with Claude Code

blove and others added 4 commits September 7, 2026 22:45
The social card is not rendered by the site's CSS. It is a separate Satori
pipeline behind Next's `ImageResponse`, and Satori cannot read woff2 or a
`next/font` handle, so the card carries its own TTFs committed under
`src/app/card/fonts/`. When the site was rethemed to the ATC language the
card's colours were hand-copied across; its fonts were not. Share a link and
the preview came back set in EB Garamond while the page it opened was Archivo
Black — two products, one link.

Rebuilds the bundle onto the site's own faces: Archivo Black for display,
Archivo 400/600 for body, JetBrains Mono unchanged for the eyebrow and pills.
EB Garamond and both Inter cuts are deleted.

Two wrinkles in the build script. Archivo Black is shipped static — it has no
`fvar` — so instancing is now conditional rather than unconditional; the
instancer does not no-op on a font with no axes. And Archivo's variable source
carries a `wdth` axis alongside `wght`, which has to be pinned like the `opsz`
pin Inter needed, or variation tables survive into the output and Satori
throws at request time.

Archivo Black is single-weight, so the `fontWeight: 700` that sat beside every
display `fontFamily` is dropped — the same removal the site's CSS made.

Verified by rendering, not just by the specs: `/opengraph-image` and a blog
card both return 200 image/png at 1200x630 from a built server, set in the
new faces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
These PDFs are the artifact a lead actually downloads from the site's lead
form, so leaving them on the old brand meant the document and the site it
came from looked like two different products.

Display type moves to Archivo Black, body to Archivo, and the old #004090
brand blue to scope navy #15253E, which is the emphasis ink everywhere small
text carries it: the cover eyebrow, the chapter eyebrow, the contents
numbers, and inline <strong>. Archivo Black ships a single weight and no
italic, so nothing pairs it with font-weight or font-style — the same
synthesis smear that was swept out of the site's CSS.

Aviation yellow appears once per page opener as a short fill under the
heading, on the cover, the contents page, and every chapter. It is never
type: at 1.84:1 on white it cannot be read.

Regenerating the prose would have meant swapping a design change for a
content change nobody asked for, so --rerender pours the committed chapters
back through the current template without calling the model. It also drops
the literal "# Chapter Title" that older runs leaked into the body directly
beneath the heading that already said it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mdToHTML had no inline-code rule and never escaped markup, so every
`<chat-message-list>` in the prose reached the reader as an empty pair of
backticks — the browser parsed the name as an unknown element, which renders
as nothing. Twelve component names were missing from the chat paper alone.

It also split paragraphs AFTER building fenced blocks, so any code sample
containing a blank line was torn in half and its second half wrapped in a <p>,
leaving the sample's own markup unescaped. The Angular template in the
chat-debug example lost both of its element lines the same way.

The source text survived in the committed HTML, so both are repaired on the
way back through --rerender rather than left in a published document with
words missing from it. mdToHTML is fixed too, so a fresh generation cannot
reintroduce either: fenced blocks are lifted out to placeholders before the
paragraph split and escaped exactly once, and inline spans are escaped.

Verified by rendering the PDFs and reading them, not by inspecting the HTML.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- icon.svg becomes the yellow squircle with an ink glyph, matching the nav
  mark. It was still a navy field with a white plane.
- --form-focus-ring was retuned to the signal yellow, which composites to
  1.19:1 on white — softer than the navy it replaced, so the halo had visibly
  thinned. Mixed at 55% it reads again while staying the signal colour. The
  focused control's indicator is its navy border at 15.37:1, so this is the
  halo around it rather than the indicator itself.
- .hero-trust was fully overridden by .hero-strip and did nothing; the class
  stays on the element for Hero.spec.tsx.
- .hero-strip's #ffffff is now explained rather than tokenised: every candidate
  token is re-pointed by the enclosing signal scope.
- PlaneMark's docblock claimed a navy field and an inherited colour, both false.
- --color-alert is recorded as reserved with no consumer, so it does not read
  as an oversight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
threadplane Ready Ready Preview Sep 8, 2026 6:51am UTC

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

The lint error was real: the fence sentinel used NUL, which trips
no-control-regex. Swapped for a Private Use Area character, which cannot
appear in prose and is not a control character.

Writing a test for that then caught a defect in the previous commit. Lifting
fenced blocks out to a sentinel meant the paragraph wrapper no longer
recognised them — it checks startsWith('<pre') — so the restored block landed
inside a <p>. `<p><pre>` is invalid, so the browser auto-closes the paragraph
and leaves a stray `</p>`: a milder version of the exact artifact the rewrite
existed to remove. The wrapper now treats the sentinel as pre-formatted.

mdToHTML has now shipped three separate silent defects into published PDFs,
every one of which DELETED text rather than merely misformatting it. It had no
spec because the module built an Anthropic client at import time, which throws
under jsdom — so the client is now lazy and the helpers are exported and
pinned. Seven cases, each one a bug that actually shipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

…can be tested

`nx test website` failed where `vitest --root apps/website` passed, and nx
swallowed the output entirely — the sign of a hard crash during collection
rather than a failing assertion. The cause was the spec importing
generate-whitepaper.ts, which pulls in puppeteer and the Anthropic SDK at
module scope.

That is also the reason these functions had no spec in the first place, and
therefore the reason three text-DELETING defects reached published PDFs
unnoticed. A pure string function should not drag a browser-automation library
into a jsdom worker, so mdToHTML and escapeHtml now live in
whitepaper-markdown.ts and the generator imports them.

Verified the spec genuinely runs rather than being skipped: 137 files / 1382
tests under `nx test website`, with the 7 markdown cases among them. The
generator still produces byte-identical HTML.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@blove
blove merged commit 7432d6d into main Sep 8, 2026
71 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant