From 6d3f50048261f7e9f44e95e49b502fbecb2a5bc3 Mon Sep 17 00:00:00 2001
From: Paul Keen <125715+pftg@users.noreply.github.com>
Date: Thu, 13 Aug 2026 12:54:16 +0200
Subject: [PATCH 1/5] fix(mermaid): hide diagram source until mermaid renders
it
`
` renders its own source as article prose until
mermaid.js swaps in the SVG. baseof.html defers mermaid.run() until
document.fonts.ready, so on a slow font load the reader gets a wall of
`flowchart TD R70["Rails 7.0 - ... "] ... classDef dead fill:#fff5f5,...`
mid-paragraph; if Caveat never resolves it never goes away.
Reported against the Rails 7 EOL post as "images have on it". The
entity itself is fine - htmlLabels is on, so mermaid decodes it to a
trailing space. The bug was only ever the flash.
Site-wide and pre-existing: shipped with the first mermaid post in May 2026.
The visual suite renders after fonts settle, so screenshots were always green
and it went unreported for three months.
Trade-off: if JS fails outright the diagram is absent rather than showing its
source. Absent beats a wall of code.
Co-Authored-By: Claude Opus 5 (1M context)
---
.okf/design/mermaid-theme.md | 49 ++++++++++++++++++++++++
themes/beaver/assets/css/single-post.css | 10 +++++
2 files changed, 59 insertions(+)
diff --git a/.okf/design/mermaid-theme.md b/.okf/design/mermaid-theme.md
index 6348b87e4..8b9525234 100644
--- a/.okf/design/mermaid-theme.md
+++ b/.okf/design/mermaid-theme.md
@@ -7,6 +7,7 @@ tags: [mermaid, fonts, design]
generated:
by: process:okf-migrate
at: 2026-07-13T00:00:00Z
+verified: { by: claude/opus-5, at: 2026-08-13T10:00:00Z }
---
# Root cause worth remembering
@@ -33,6 +34,54 @@ last-character clipping. The fix in `baseof.html`:
`themes/beaver/layouts/_markup/render-codeblock-mermaid.html`, which sets
`features.mermaid`.
+# The unrendered-source flash (fixed 2026-08-13)
+
+`
` renders its own SOURCE as article prose until
+mermaid.js swaps in the SVG - and because `baseof.html` defers `mermaid.run()`
+until `document.fonts.ready`, that window is as long as the Caveat load takes.
+If the font never resolves, the wall of `flowchart TD ... classDef dead
+fill:#fff5f5,...` never goes away.
+
+Reported by the owner as "images have ` ` on it" - the ` ` we append
+to labels is emitted by Hugo as ` `, so it sits in that raw text as a
+literal entity. The entity itself is FINE once rendered: htmlLabels is on, so
+mermaid decodes it to a trailing space. The bug was only ever the flash.
+
+Fix is one rule in `themes/beaver/assets/css/single-post.css` - mermaid stamps
+`data-processed` on each node it renders:
+
+```css
+.mermaid:not([data-processed]) { display: none; }
+```
+
+Trade-off accepted: if JS fails outright the diagram is absent rather than
+showing its source. Absent beats a wall of code in the middle of a paragraph.
+This was site-wide and pre-existing - it shipped with the first mermaid post in
+May 2026 and went unreported for three months because it only bites on a slow
+font load.
+
+# Mobile legibility: width comes from COLUMNS, not label length
+
+Mermaid wraps node labels at ~200px, so a long label never widens a diagram -
+the number of PARALLEL COLUMNS does. Three sibling nodes feeding one gate
+renders ~800px wide. Because the SVG scales to the content column, wide means
+small: `renderedPx = minFontSize * (displayedWidth / viewBoxWidth)`. At a
+390px viewport a 797px-wide diagram renders 8.88px text, under the 9px floor
+`bin/check-svg-floor` enforces for course SVGs.
+
+Measured 2026-08-13 while backfilling diagrams into the August blog cluster:
+
+| Shape | viewBox | rendered @390px |
+|---|---|---|
+| 3 siblings -> 1 gate (fan-in) | 797px | 8.88px - FAILS |
+| 2 columns (chain + branch) | 545px | 13.0px |
+| single vertical column | 272px | 20px (unscaled) |
+
+Fix by chaining vertically (`A --> B --> C`, or `~~~` for unconnected nodes),
+not by shrinking labels. Note the trade: a pure single column renders at full
+size on mobile but sits as a narrow ribbon in the 684px desktop content
+column. Two columns is usually the balance point.
+
# Brand rule
Handwritten typography in diagrams is a non-negotiable brand identity -
diff --git a/themes/beaver/assets/css/single-post.css b/themes/beaver/assets/css/single-post.css
index 4d55cc372..1dab4c842 100644
--- a/themes/beaver/assets/css/single-post.css
+++ b/themes/beaver/assets/css/single-post.css
@@ -232,6 +232,16 @@
-webkit-overflow-scrolling: touch;
margin: 1.5rem 0;
}
+/* Until mermaid.js swaps in the SVG, the div renders its own SOURCE as article
+ prose - `flowchart TD R70["Rails 7.0 - ... "] ... classDef dead
+ fill:#fff5f5,...`. baseof.html defers mermaid.run() until
+ document.fonts.ready, so on a slow font load that wall of code is what the
+ reader gets; if Caveat never resolves it never goes away. Reported on the
+ Rails 7 EOL post 2026-08-13 as "images have on it".
+ mermaid sets data-processed on each node it renders. */
+.mermaid:not([data-processed]) {
+ display: none;
+}
.mermaid svg {
/* Keep natural width so labels stay legible; let .mermaid parent scroll
horizontally on narrow viewports rather than squashing text. */
From 9ccfe5ec1fcadaf5e0299eb2d0551fa973cd2c45 Mon Sep 17 00:00:00 2001
From: Paul Keen <125715+pftg@users.noreply.github.com>
Date: Thu, 13 Aug 2026 12:54:32 +0200
Subject: [PATCH 2/5] feat(blog): enforce in-body visuals, add diagrams to the
August cluster
CLAUDE.md's cognitive-load gate wants a hero visual over 800 words. Nothing
enforced it: diagram_rendering_test.rb proves diagrams RENDER, not that posts
HAVE them. 25 of the 31 long posts published since 2026-04 shipped with none;
only 2 posts in all of 2026 carried an in-body visual.
bin/check-post-visuals is a ratchet, not a clean gate - 81 legacy posts are
over the line and nobody is backfilling them. It fails only when the count
rises, so new work is blocked while the backlog burns down. Same shape as
bin/check-svg-floor. Floor: 78.
Diagrams added (all verified rendered at 1280x800 and 390x844, 0 console
errors):
- CVE-2026-66066: three-condition exposure funnel
- Lovable->Rails: transplant-vs-rebuild fork
- Kamal: the boot barrier where job hosts block on first healthy web
Mermaid width comes from parallel COLUMNS, not label length - labels wrap at
~200px. A 3-siblings-into-1-gate fan-in renders 797px wide and measures
8.88px at 390px, under the 9px floor bin/check-svg-floor applies. Chained
them vertically instead. Node shapes are rects per the house rule in
.okf/design/mermaid-theme.md.
Co-Authored-By: Claude Opus 5 (1M context)
---
.okf/build/test-gates.md | 1 +
bin/check-post-visuals | 38 +++++++++++++++++++
.../index.md | 27 +++++++++++++
.../index.md | 26 +++++++++++++
.../index.md | 24 ++++++++++++
docs/workflows/blog-pipeline.md | 6 +++
6 files changed, 122 insertions(+)
create mode 100755 bin/check-post-visuals
diff --git a/.okf/build/test-gates.md b/.okf/build/test-gates.md
index 557a42f81..6db1d40c8 100644
--- a/.okf/build/test-gates.md
+++ b/.okf/build/test-gates.md
@@ -17,6 +17,7 @@ verified: { by: claude/fable-5, at: 2026-08-01T11:30:00Z }
| `bin/rake test:critical` | Critical Minitest suite (34 runs / 53 screenshots), ~81s host / ~46s Docker since the 2026-08-01 skip_area fix | At component/task milestones and before every commit outside sprint micro-commit trains |
| `bin/test` | Visual regression on the host (baselines in `macos/` on a Mac; on Linux, comparable to `linux/` when run through `bin/setup-test-env`'s pinned stack) | ONCE at PR prep (branch head, before `gh pr create`) or on Paul's explicit confirmation - NOT per commit (Paul 2026-07-31: qtest is the routine gate) |
| `bin/dtest` | Same suite in Linux/Docker (baselines in `linux/`) - CI runs Linux | Same trigger as bin/test; a PR must never open without this leg (green-locally / red-in-CI otherwise) |
+| `bin/check-post-visuals` | Ratchet: counts blog posts over 800 words with no mermaid/SVG/image. Fails when the count exceeds `FLOOR` (78 as of 2026-08-13) | Before publishing any post. Added 2026-08-13 - `diagram_rendering_test.rb` only proves diagrams RENDER, nothing proved posts HAVE them, and 25 of the 31 long posts published since 2026-04 shipped with none |
`bin/qtest` page keys mirror `themes/beaver/assets/css/pages/*.css` basenames
AND `critical/-critical.css` basenames - the two sets differ (e.g.
diff --git a/bin/check-post-visuals b/bin/check-post-visuals
new file mode 100755
index 000000000..13b0b9afd
--- /dev/null
+++ b/bin/check-post-visuals
@@ -0,0 +1,38 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+#
+# CLAUDE.md's cognitive-load gate wants a hero visual in the first fold for
+# posts over 800 words. Nothing enforced it: diagram_rendering_test.rb checks
+# that diagrams RENDER, not that posts HAVE them. 25 of the 31 long posts
+# published since 2026-04 shipped with no visual at all.
+#
+# Ratchet, not a clean gate - 81 legacy posts are already over the line and
+# nobody is backfilling them. The count may only go down, so a new post
+# without a visual fails the build while the backlog burns off at its own pace.
+#
+# Usage: bin/check-post-visuals [dir] (default: content/blog)
+# POST_VISUALS_LIST=1 to print the full burn-down list.
+
+FLOOR = 78
+MIN_WORDS = 800
+root = ARGV[0] || "content/blog"
+
+fails = Dir.glob(File.join(root, "*", "index.md")).sort.filter_map do |path|
+ body = File.read(path).split(/^---$/, 3)[2].to_s
+ words = body.split.size
+ next if words < MIN_WORDS
+ next if body.match?(/^```mermaid|