diff --git a/.impeccable/config.json b/.impeccable/config.json index ac2f11dbd..0c53702f1 100644 --- a/.impeccable/config.json +++ b/.impeccable/config.json @@ -1,7 +1,9 @@ { "detector": { "ignoreRules": [], - "ignoreFiles": [], + "ignoreFiles": [ + ".stitch/designs/*" + ], "ignoreValues": [ { "rule": "overused-font", 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/.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/.okf/log.md b/.okf/log.md index 3631c41d0..f67fc3bf2 100644 --- a/.okf/log.md +++ b/.okf/log.md @@ -1164,3 +1164,62 @@ calls → pause, re-open A + C." Clock starts at first post or first send. names" was unsatisfiable; "2-3 past-client referral asks" survives because a referral source need not be the ICP. Demote, don't delete, when a weaker form retains value. + +## 2026-08-13 - Blog in-body visuals: a gate nothing enforced + +Audited the four posts published 2026-08-07/08 (Kamal multi-server, Lovable->Rails +migration, Rails 7 EOL, CVE-2026-66066). The prose gates had all held: zero em +dashes, zero bold inline-header lists, 2-8% paragraph cap breaks against the +39-58% of the failed TDD/XP cluster, varied opener shapes, and every mechanical +pre-publish check green. + +The gap was visuals. Three of four had none, and the archive said why: only 2 +posts in all of 2026 carried an in-body visual, while 25 of the 31 long posts +published since 2026-04 carried none. CLAUDE.md's cognitive-load gate requires a +hero visual over 800 words; nothing checked it, because +`test/unit/diagram_rendering_test.rb` proves diagrams RENDER, not that posts HAVE +them. + +**Durable rules:** +1. **Text gates automate, visual gates rot.** Banned words and frontmatter shape + are greppable so they survive time pressure; "does this need a diagram" is + judgment and silently drops. Any gate stated only in prose will decay - give + it a ratchet script or expect it unenforced. +2. **Ratchet over backfill.** 81 legacy posts are over the line and nobody is + backfilling them. `bin/check-post-visuals` fails only when the count RISES, + so new work is blocked while the backlog burns down at its own pace - same + shape as `bin/check-svg-floor`. +3. **Read the OKF bundle before authoring diagrams, not after.** Skipping the + session-start consume meant shipping five diagrams with diamond decision + nodes, which `design/mermaid-theme.md` bans outright. Caught only on the + pre-commit maintain pass. +4. **A cover chip is a claim with an expiry.** The Rails 7 EOL cover read "7.2 + SUPPORT ENDS August 9, 2026" and the post was written in future tense; both + went stale four days after publishing. Date-bearing covers and prose need a + recheck whenever the date passes. +5. **The gem never varies.** `.stitch/design.md` slot 4 is explicit - the + low-poly ruby is the brand throughline across ALL Ruby/Rails posts. + "Cover sameness" is the design working, not a defect. + +## 2026-08-13 - Mermaid rendered its own source as prose until fonts loaded + +Owner reported "images have   on it" on the Rails 7 EOL post. Reproduced by +stalling `document.fonts.ready`: `
` displays its raw source +as article text - `flowchart TD R70["Rails 7.0 - ... "] ... classDef dead +fill:#fff5f5,...` - because `baseof.html` defers `mermaid.run()` until fonts +resolve and no CSS hid the div in the meantime. Fixed with one rule: +`.mermaid:not([data-processed]) { display: none; }`. + +**Durable rules:** +1. **The reported symptom is rarely the bug.** "` ` in the image" was not an + entity-escaping bug - the entity decodes correctly once htmlLabels render it. + It was a flash-of-unrendered-source bug that happened to expose the entity. + Chasing the escaping would have produced a fix that changed nothing. +2. **Deferring render needs a matching hide.** Any "wait for X, then swap the + DOM" pattern leaves the pre-swap content visible for the length of X. The + font-gated `mermaid.run()` was added to fix label CLIPPING and silently + created a worse visual defect on the slow path. +3. **Site-wide and three months old.** It shipped with the first mermaid post in + May 2026 and nobody saw it, because it only bites on a slow font load - the + visual suite renders after fonts settle, so screenshots were always green. + A suite that waits for the happy path cannot see the unhappy one. 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| FLOOR + puts "post-visuals: #{fails.size} posts over #{MIN_WORDS} words have no visual (floor #{FLOOR})." + puts "Add a diagram/image, or if you removed one deliberately, raise FLOOR with a reason." + fails.last(10).each { |p, w| puts format(" %5dw %s", w, p) } + exit 1 +end + +puts "post-visuals: #{fails.size} of #{Dir.glob(File.join(root, '*', 'index.md')).size} posts lack a visual (floor #{FLOOR})" +fails.each { |p, w| puts format(" %5dw %s", w, p) } if ENV["POST_VISUALS_LIST"] == "1" +puts "post-visuals: floor is loose, lower FLOOR to #{fails.size}" if fails.size < FLOOR diff --git a/content/blog/kamal-2-multi-server-deployment-complete-guide/index.md b/content/blog/kamal-2-multi-server-deployment-complete-guide/index.md index fc8242996..250285a8f 100644 --- a/content/blog/kamal-2-multi-server-deployment-complete-guide/index.md +++ b/content/blog/kamal-2-multi-server-deployment-complete-guide/index.md @@ -101,6 +101,33 @@ Once workers live on their own hosts, queue concurrency and retry behavior stop Next, Kamal stages assets and SSL certs onto every host in a separate pass. Only then does booting start, and one detail here saves you a confused hour: non-primary roles wait. Kamal creates a health barrier, and job hosts block until the first web container reports healthy - the log says `Waiting for the first healthy web container before booting job on 10.0.0.21...`. If that first web container never turns healthy, your job hosts never boot at all. The image gets tagged `latest` only after every host is up. +```mermaid +flowchart TD + B["Build image once,
push to registry "] + P["Pull it on every app host "] + S["Stage assets and SSL certs
on every host "] + W["Boot web hosts
proxy polls /up for 30s "] + BAR["First web container
healthy? "] + J["Boot job hosts
no proxy, so Docker HEALTHCHECK
or a 7s readiness_delay "] + STUCK["Job hosts never boot "] + TAG["Tag the image latest "] + + B --> P --> S --> W --> BAR + BAR -->|"yes "| J + J --> TAG + BAR -->|"never "| STUCK + + classDef step fill:#faf7f2,stroke:#555,stroke-width:2px,color:#1a1a1a + classDef gate fill:#f5e9ff,stroke:#7c3aed,stroke-width:2.5px,color:#1a1a1a + classDef bad fill:#fff5f5,stroke:#cc342d,stroke-width:2.5px,color:#1a1a1a + classDef ok fill:#f0f9f0,stroke:#2e7d32,stroke-width:2.5px,color:#1a1a1a + + class B,P,S,W,J step + class BAR gate + class STUCK bad + class TAG ok +``` + Health checks also differ by role. Web containers get polled by kamal-proxy on `/up` once a second until `deploy_timeout` (default 30s). Job containers have no proxy, so Kamal uses the Docker `HEALTHCHECK` from your image - and if there isn't one, it waits `readiness_delay` (7 seconds) and declares victory. A worker that crashes at second 8 still counts as a successful deploy, which is worth fixing with a real `HEALTHCHECK` before you trust rolling deploys. When a deploy stalls on "target failed to become healthy", [we wrote a full debugging guide for that error](/blog/solving-kamals-target-failed-become-healthy/). One more thing lives in exactly one place: the deploy lock, held over SSH on the primary host only. If the primary host is down, you can't take the lock, which means you can't deploy the healthy hosts either. [2.12.0](https://github.com/basecamp/kamal/releases/tag/v2.12.0) added `--lock-wait` for the other lock problem - two CI runs racing each other - which pairs well with [deploying from GitHub Actions](/blog/automate-your-deployments-with-kamal-2-github-actions-devops-development/). diff --git a/content/blog/migrate-lovable-replit-app-to-rails/index.md b/content/blog/migrate-lovable-replit-app-to-rails/index.md index 363199780..d6bf94e9c 100644 --- a/content/blog/migrate-lovable-replit-app-to-rails/index.md +++ b/content/blog/migrate-lovable-replit-app-to-rails/index.md @@ -46,6 +46,32 @@ Here's how it usually splits: One decision drives everything else. If the schema is sane and the data is real, you're doing a backend transplant and keeping the UI. If the schema is a mess and the "logic" is a thin wrapper over AI-generated CRUD, you're rebuilding, and the old app is a spec, not a codebase. +```mermaid +flowchart TD + START["Dump the schema,
read it before deciding "] + Q["Schema sane,
data real? "] + T["Backend transplant:
port schema and rows,
point the React UI at Rails "] + R["Rebuild in Rails:
load the rows, treat the
old app as the spec "] + BOTH["Either way you rebuild
auth and payments "] + + START --> Q + Q -->|"yes "| T + Q -->|"no, it is thin CRUD "| R + T --> BOTH + R --> BOTH + + classDef step fill:#faf7f2,stroke:#555,stroke-width:2px,color:#1a1a1a + classDef gate fill:#f5e9ff,stroke:#7c3aed,stroke-width:2.5px,color:#1a1a1a + classDef keep fill:#f0f9f0,stroke:#2e7d32,stroke-width:2.5px,color:#1a1a1a + classDef redo fill:#fff5f5,stroke:#cc342d,stroke-width:2.5px,color:#1a1a1a + + class START step + class Q gate + class T keep + class R redo + class BOTH redo +``` + We wrote a longer field guide to that call in [the vibe coding crisis: AI code debt](/blog/vibe-coding-crisis-ai-code-debt/). Read the schema before you decide. Everything downstream forks here. This is also the point where you decide whether to run the migration yourself. If your team reads Postgres comfortably and has shipped auth before, keep going. If not, [our vibe code rescue service](/services/vibe-code-rescue/) opens with a 48-hour audit and quotes a fixed price from it. diff --git a/content/blog/rails-7-eol-unpatched-security-exposure/cover.png b/content/blog/rails-7-eol-unpatched-security-exposure/cover.png index 2a094f0f0..0de1098a3 100644 Binary files a/content/blog/rails-7-eol-unpatched-security-exposure/cover.png and b/content/blog/rails-7-eol-unpatched-security-exposure/cover.png differ diff --git a/content/blog/rails-7-eol-unpatched-security-exposure/index.md b/content/blog/rails-7-eol-unpatched-security-exposure/index.md index b280baec0..5b59e9654 100644 --- a/content/blog/rails-7-eol-unpatched-security-exposure/index.md +++ b/content/blog/rails-7-eol-unpatched-security-exposure/index.md @@ -21,12 +21,31 @@ cover_image_alt: "Rails 7 end of life cover - no patch for CVE-2026-66066 on 7.1 canonical_url: https://jetthoughts.com/blog/rails-7-eol-unpatched-security-exposure/ --- -On July 29 the Rails security team fixed a CVSSv4 9.5 vulnerability in Active Storage - an unauthenticated upload that reads files off your server and [may enable code execution](https://github.com/rails/rails/security/advisories/GHSA-xr9x-r78c-5hrm). Three branches got a patched release that day: 7.2, 8.0, and 8.1. One of those three, 7.2, reaches the end of its security support on August 9, 2026 - eleven days after carrying the patch, upstream stops shipping it fixes. Rails 7.1 and everything below it got nothing. +On July 29 the Rails security team fixed a CVSSv4 9.5 vulnerability in Active Storage - an unauthenticated upload that reads files off your server and [may enable code execution](https://github.com/rails/rails/security/advisories/GHSA-xr9x-r78c-5hrm). Three branches got a patched release that day: 7.2, 8.0, and 8.1. One of those three, 7.2, reached the end of its security support on August 9, 2026 - eleven days after carrying the patch, upstream stopped shipping it fixes. Rails 7.1 and everything below it got nothing. That's the concrete version of Rails 7 end of life. Rails 7.1 had [finished its security support period](https://rubyonrails.org/2025/10/29/new-rails-releases-and-end-of-support-announcement) by October 2025, so when CVE-2026-66066 landed, the backport list stopped at 7.2. If your app sits on 7.1 or lower and processes image variants, there's no version on your branch to bump to. The fix lives on branches you're not on. +```mermaid +flowchart TD + R70["Rails 7.0 - no patch, dead since Apr 2025 "] + R71["Rails 7.1 - no patch, dead since Oct 2025 "] + R72["Rails 7.2 - patched, then dead Aug 9, 2026 "] + R80["Rails 8.0 - patched, supported to Nov 2026 "] + R81["Rails 8.1 - patched, supported to Oct 2027 "] + + R70 ~~~ R71 ~~~ R72 ~~~ R80 ~~~ R81 + + classDef dead fill:#fff5f5,stroke:#cc342d,stroke-width:2.5px,color:#1a1a1a + classDef soon fill:#fffbe6,stroke:#bf8a00,stroke-width:2px,color:#1a1a1a + classDef live fill:#f0f9f0,stroke:#2e7d32,stroke-width:2.5px,color:#1a1a1a + + class R70,R71,R72 dead + class R80 soon + class R81 live +``` + The CVE itself already has a [full writeup](/blog/rails-cve-2026-66066-active-storage-rce/): mechanism, detection inside a running app, the libvips 8.13 requirement, the secret rotation order. This post covers the problem that outlives this CVE. You're on an unsupported Rails, the next critical will skip you the same way, and there are four ways out. ## Skipped on schedule @@ -72,7 +91,7 @@ Last, sort that list by whether this specific CVE can reach each app. The [CVE w Upgrade if you can. Every other exit on this list buys time and leaves the branch dead. -Pick the target with the support clock in view. 7.2 is the smallest hop from 7.1, but its [security support ends August 9, 2026](https://rubyonrails.org/2025/10/29/new-rails-releases-and-end-of-support-announcement), so on its own it trades one dead branch for another; paired with the paid 7.2 LTS line in exit 2 it holds up. 8.0 holds until November 7, 2026, three months away. +Pick the target with the support clock in view. 7.2 is the smallest hop from 7.1, but its [security support ended August 9, 2026](https://rubyonrails.org/2025/10/29/new-rails-releases-and-end-of-support-announcement), so on its own it now trades one dead branch for another; paired with the paid 7.2 LTS line in exit 2 it still holds up. 8.0 holds until November 7, 2026. 8.1 is the only hop that buys real time - security fixes run to October 2027. Budget the Ruby bump into the same plan: Rails 8 needs Ruby 3.2 or newer, and a 7.1 app often isn't there yet. @@ -123,6 +142,10 @@ Match each app from your fleet sweep to a row - a mixed portfolio lands in sever | Internal tool behind SSO, no public uploads | Exit 4 now, fold exit 1 into next quarter's maintenance | | App with a real decommission date | Exit 4, with the date and the accepted risk written down | +One app you can match to a row yourself. A dozen of them across three dead branches, against a roadmap and a budget that are already committed, is a sequencing call someone senior has to make and then defend when it pushes a quarter of feature work. + +If nobody holds that seat today, it's the [fractional CTO](/services/fractional-cto/) work we get called into, and it starts with the same fleet sweep you just ran. Worth saying plainly: that engagement ranks the queue and owns the argument, it doesn't write your upgrade commits. + ## Put the dates where you'll see them The support deadlines from exit 1 belong in the calendar, each with an owner - treat them like TLS certificate expiries, not facts you rediscover inside an advisory email. diff --git a/content/blog/rails-cve-2026-66066-active-storage-rce/index.md b/content/blog/rails-cve-2026-66066-active-storage-rce/index.md index 633630d8d..4edf7547d 100644 --- a/content/blog/rails-cve-2026-66066-active-storage-rce/index.md +++ b/content/blog/rails-cve-2026-66066-active-storage-rce/index.md @@ -43,6 +43,30 @@ The advisory sets three conditions, and together they are the whole test: 2. The app accepts **image uploads from untrusted users**. 3. The environment lacks protection against unfuzzed libvips operations: **libvips older than 8.13**, or 8.13+ with the untrusted-operation block left off. +```mermaid +flowchart TD + C1["Variant processor is vips? "] + C2["Untrusted image uploads? "] + C3["libvips under 8.13? "] + YES["Exposed. Run the runbook,
rotate the secrets. "] + NO["Not reachable by this CVE.
Patch on your next deploy. "] + + C1 -->|"yes "| C2 + C2 -->|"yes "| C3 + C3 -->|"yes "| YES + C1 -->|"no "| NO + C2 -->|"no "| NO + C3 -->|"no "| NO + + classDef gate fill:#f5e9ff,stroke:#7c3aed,stroke-width:2.5px,color:#1a1a1a + classDef bad fill:#fff5f5,stroke:#cc342d,stroke-width:2.5px,color:#1a1a1a + classDef ok fill:#f0f9f0,stroke:#2e7d32,stroke-width:2.5px,color:#1a1a1a + + class C1,C2,C3 gate + class YES bad + class NO ok +``` + The advisory closes one door explicitly: "Generating variants is not a separate requirement." Grepping the codebase for `.variant(` calls and finding none does not clear you. If the three conditions hold, you're exposed. Two of the three are checkable by command. Run these inside the running app; laptop versions prove nothing. diff --git a/docs/projects/2607-vibe-code-rescue/backlog.md b/docs/projects/2607-vibe-code-rescue/backlog.md index 335374f30..2aae73057 100644 --- a/docs/projects/2607-vibe-code-rescue/backlog.md +++ b/docs/projects/2607-vibe-code-rescue/backlog.md @@ -45,6 +45,7 @@ Strategy/gates/Paul's desk live in [`operation-runbook.md`](operation-runbook.md | T3 | Gmail warm-source pass (Paul consent) → warm rows | Blocked-on-Paul (optional, non-blocking) | | T8 | Discovery kit (call script + audit template) | ✓ Done (`rescue-sprint/discovery-kit.md`) | | T9 | Objection + FAQ + follow-up bank → `rescue-sprint/objection-followup-bank.md` | **Ready** | +| T10 | Split business/strategy docs out of the public repo - [#449](https://github.com/jetthoughts/jetthoughts.github.io/issues/449). Touches this project's `20-29-strategy/` files, so 2607 tracks it. | Blocked-on-Paul (org decision, not a refactor) | --- diff --git a/docs/workflows/blog-pipeline.md b/docs/workflows/blog-pipeline.md index f21c9bf18..0d24f265e 100644 --- a/docs/workflows/blog-pipeline.md +++ b/docs/workflows/blog-pipeline.md @@ -403,6 +403,12 @@ Before flipping draft: false or before any LinkedIn/external promotion drives tr 6. dev.to import ICP gate (only if `source: dev_to`): - [ ] Audit the post against `docs/90-99-content-strategy/strategy-analysis/90.10-icp-primary-website-target.md`. If 80+/100 AI-feel or off-thesis, rewrite for the ICP-E or flip to `draft: true`. +7. In-body visual (BLOCKING for posts over 800 words): + - [ ] `ruby bin/check-post-visuals` exits 0. The floor is a ratchet - a new post with no diagram/image pushes the count over it and fails. + - [ ] Measure the diagram at 390px before committing: `renderedPx = minFontSize * (displayedWidth / viewBoxWidth)` must be >= 9 (same floor `bin/check-svg-floor` applies to course SVGs). Caught 2026-08-13: a first-draft LR flowchart measured 8.88px and was illegible on mobile. + - [ ] Width is driven by PARALLEL COLUMNS, not label length - mermaid wraps labels at ~200px. Three sibling nodes feeding one gate renders ~800px wide and fails the floor. Chain them vertically (`A --> B --> C`, or `~~~` for unconnected nodes) to stay under ~550px. + - [ ] The diagram earns its place: it shows something the surrounding prose and tables do NOT already state. A diagram restating an adjacent table is decorative - delete it. + If any of the above checks fail, fix before proceeding to STEP 7. STEP 7 — VALIDATE 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. */