Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions .okf/build/hugo-stats-th-classes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
type: Gotcha
title: Classes used only on <th> get purged from production CSS
description: Hugo 0.165 writeStats records no class attributes on <th>, so any selector whose class appears only on a th is stripped by PurgeCSS in production - and the visual gate cannot catch it because baseline and candidate are both built purged.
tags: [css, purgecss, hugo, false-green, layouts]
status: stable
generated: { by: claude/opus-5, at: 2026-08-21T18:12:00Z }
verified:
- { by: claude/opus-5, at: 2026-08-21T18:12:00Z }
timestamp: 2026-08-21T18:12:00Z
---

# The trap

PurgeCSS keeps a selector only when the class appears in `hugo_stats.json`,
which Hugo writes from the rendered HTML. On Hugo 0.165 that file records the
`th` **tag** but not class attributes on `th` elements. A class used only on a
`<th>` is therefore invisible to PurgeCSS and its rule is deleted from the
production bundle.

Probe that established it (2026-08-21, `layouts/next/landing.html`):

| Class | Element it sits on | In hugo_stats.json |
|---|---|---|
| `rr-td-muted` | `<td>` | yes |
| `rr-table` | `<table>` | yes |
| `rr-btn-primary` | `<a>` | yes |
| `rr-th-ours` | `<th>` | **no** |
| `rr-th-theirs` | `<th>` | **no** |

Development builds hide it completely: `postcss.config.js` sets
`isDevelopment` from `HUGO_ENVIRONMENT === "development"` and skips PurgeCSS
entirely, so `bin/hugo-build` renders the styling correctly while production
ships it unstyled.

# Why no gate catches it

The visual regression suite compares a baseline against a candidate that were
**both** built the same way, so a rule that is purged in both is invisible to
it. `css_orphan_guard_test.rb` does not help either - it asks whether a FILE
is reachable from a template, never whether a SELECTOR survived the purge.
This class of defect reaches production green.

# What to do instead

Style the column by position, or move the class onto a child element:

```css
/* works: the selector matches on tag + position, no class lookup */
.rr-table thead th:nth-child(2) { color: var(--rr-ruby); }
```

```html
<!-- also works: the class rides a span, which writeStats does record -->
<th scope="col"><span class="rr-th-ours">Fractional CTO</span></th>
```

Verify on a PRODUCTION build, never a dev one: grep the fingerprinted bundle
under the production destination for the selector, and grep the rendered HTML
to confirm no orphan class attribute remains.

# The same class of defect elsewhere

Two more ways a /next/ rail change goes green while being wrong:

- **`page/site-scripts` in a landing baseof.** Its only payload is
`navigation.js`, which unconditionally binds to `.js-mobile-menu-opener`
from the site header partial. A layout that renders its own chrome and
drops that partial throws `Cannot read properties of null` on every page
load - and no test asserts a clean console, so nothing fails.
- **Computed `resources.Get` paths.** `css_orphan_guard_test.rb` scans for
the literal `resources.Get "css/…"` string. A `printf`-built path silently
removes the file from the guard's reachable set, so the guard keeps passing
while no longer guarding anything. Keep one literal `resources.Get` per
file, selected through a dict, when a template serves several skins.
1 change: 1 addition & 0 deletions .okf/build/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
* [Test gates](test-gates.md) - the local suites, when each is a commit blocker, the 0.0001 default tolerance, the SECTION_CONFIGS shield, why a green run never refreshes a baseline, and why below-fold content is invisible at any tolerance, bin/record-baselines for accepting only the baselines you meant to move, and why a deleted source file still serves from every local _dest/ tree, plus the NULL CHANGE - a diff that passes every gate and alters nothing - and what `okf_validate` actually guards (shape, not truth; error-only conformance) with the two-spec trap
* [CI gates](ci-gates.md) - what GitHub Actions enforces: build, unit, path-scoped link check (visual regression is report-only), and what gates a PR never sees
* [Template PDFs](pdf-templates.md) - regenerating the downloadable course PDFs
* [Classes only on &lt;th&gt; get purged](hugo-stats-th-classes.md) - Hugo 0.165 writeStats records no class attrs on `<th>`, so PurgeCSS deletes those rules from the production bundle while dev builds look fine and the visual gate stays green; also two sibling false-greens on the /next/ rail (site-scripts in a landing baseof, computed `resources.Get` paths defeating the orphan guard)
44 changes: 44 additions & 0 deletions .okf/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,50 @@ any truthy value, `test/support/setup_snap_diff.rb:28` enters record mode only
on the literal `"true"`. `=1` on qtest therefore gets the worst of both: the
suite still compares and fails, and the cleanup is skipped. That is the
mechanism behind the older "the flag appears to be ignored on qtest" note.
## 2026-08-21 - register pilot A, and three ways a /next/ change ships green and wrong

Pilot A ("Rescue Room") landed on the rail at
`/next/pilots/rescue-room/fractional-cto/`: Paul's approved Claude Design
artifact translated onto `layouts/next/landing.html`, all copy in stub
frontmatter so pilots B and C reuse the template, self-hosted Poppins (a
fonts.googleapis.com link would put a third-party fetch inside the screenshot
runs). New concept `build/hugo-stats-th-classes.md` records the finding that
adversarial review surfaced and the two siblings it belongs with.

The finding: **Hugo 0.165 writeStats records no class attributes on `<th>`**,
so `.rr-th-ours` / `.rr-th-theirs` were absent from `hugo_stats.json` and
PurgeCSS deleted them from the production bundle - while `rr-td-muted` on a
`<td>` in the same table was recorded fine. Dev builds skip PurgeCSS entirely,
so the page looked correct locally and shipped unstyled. **No gate catches
this**: the visual suite compares a baseline and a candidate that were both
built purged, and `css_orphan_guard_test` asks whether a FILE is reachable,
never whether a SELECTOR survived. Fix is positional (`th:nth-child(2)`) or a
class on a child span. Two siblings, same shape: `page/site-scripts` in a
landing baseof throws on every load (navigation.js binds unconditionally to
header nodes the layout dropped, and nothing asserts a clean console), and a
`printf`-computed `resources.Get` path silently drops a file from the orphan
guard's reachable set so the guard passes while guarding nothing.

Two corrections to how the evidence was gathered, both worth keeping.
`FORCE_SCREENSHOT_UPDATE=1` does NOT force a re-record - in `bin/qtest` its
only effect is skipping the post-run `git checkout` restore - so a
byte-identical PNG under that flag proves nothing. And the system tests serve
`_dest/public-test-local` while qtest's build step refreshes
`_dest/public-test`; a probe verified in the wrong tree returns a meaningless
pass. Related, still open: a marker placed in an in-frame table cell did not
fail the desktop screenshot test even with the served tree confirmed to
contain it, so the capture path is under separate diagnosis and pilots B/C are
on hold until it is understood.

Also: `bin/qtest` now maps `themes/beaver/static/(css|fonts)/` to `:all` - a
webfont change alters text rendering on every page but mapped to nil, the same
false-green class the untracked-file guard was added for. And a real person's
words are not copy to tighten: the design blueprint had smoothed the Wozniak
Clutch quote ("Their team was also detailed and precise, helping us to find
problems..." became "They were detailed and precise, helping us find
problems...") and it was carried through with a "verbatim" claim that nobody
had checked. The pilot now asserts the rendered blockquote against
`data/testimonials.yaml`.

## 2026-08-21 - anatomy settled, register under test: the pilots flow

Expand Down
8 changes: 8 additions & 0 deletions bin/qtest
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ PAGE_TESTS = {
"friday-report" => "friday_report",
# v2 clean-slate rail (ADR-0006): layouts/next/* + pages/next-pilot.css
"next-pilot" => "next_pilot",
# Design-register pilots on the same rail: pages/next-<register>.css
"next-rescue-room" => "next_rescue_room",
"simple-page" => "privacy_policy",
# critical/privacy-policy-critical.css maps by basename; alias it to the
# simple-page tests so a touch there doesn't abort as an unknown key
Expand Down Expand Up @@ -76,6 +78,12 @@ def pages_for(file)
when %r{^themes/beaver/assets/css/critical/([^/]+)-critical\.css$} then [Regexp.last_match(1)]
when %r{^themes/beaver/layouts/blog/}, %r{^content/blog/} then %w[blog-list blog-single]
when %r{^layouts/next/}, %r{^content/next/} then PAGE_TESTS.keys.grep(/^next-/) # every v2 key; shared v2 templates affect them all
# Self-hosted webfonts and the @font-face files that declare them change how
# EVERY page renders text, but they live outside the asset CSS tree, so the
# rules below never saw them - a fonts-only diff mapped to nil and qtest
# exited 0 having tested nothing. Same false-green class the untracked-file
# guard above was added for.
when %r{^themes/beaver/static/(css|fonts)/} then :all
# Terminal: root layouts/ had NO case at all, so a root-layouts-only diff
# mapped to nil and qtest exited 0 having tested nothing (false green).
when %r{^themes/beaver/assets/css/}, %r{^themes/beaver/layouts/}, %r{^layouts/} then :all
Expand Down
170 changes: 170 additions & 0 deletions content/next/pilots/rescue-room/fractional-cto.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
---
title: Fractional CTO (Rescue Room pilot)
description: Design-register pilot A - warm humanist "Rescue Room" skin over the settled fractional-CTO landing anatomy.
layout: landing
type: next
# Drives both the CSS file and the bundle name in layouts/next/landing.html,
# so pilots B and C reuse this template by changing one line.
register: rescue-room
# Self-hosted display face for this register (themes/beaver/static/). Another
# register omits this key and ships no webfont at all.
font:
css: /css/fonts-poppins.css
preload: /fonts/poppins-latin-700.woff2

nav:
links:
- name: Services
url: /services/
- name: Use Cases
url: /use-cases/
- name: Blog
url: /blog/
- name: Course
url: /course/tech-for-non-technical-founders-2026/
- name: Clients
url: /clients/
cta:
name: Book a free consultation
url: /free-consultation/

hero:
eyebrow: Fractional CTO
headline: A technical partner in days, not months of hiring
subhead: Your dev shop isn't working. Your investors want a technical update. You need someone who can audit the code, fix the team, and give you a straight answer.
primary:
name: Book a free consultation
url: /free-consultation/
secondary:
name: Book a 30-min call
url: /contact-us/
derisk: You keep the written assessment whether or not we work together.
# Markdown, rendered inline - the bold carries the emphasis the blueprint
# gave <strong>. Clutch rating is linked to the profile (claims-canon).
stats:
- Starts in **days**
- No equity, no lock-in
- "[**4.8**/5 on Clutch](https://clutch.co/profile/jetthoughts)"

monthOne:
headline: What a fractional CTO does in month one
items:
- Reads the codebase and tells you what state it's actually in
- Sits in on your standups and tells you which ones are theatre
- Writes the technical section of your investor update
- Decides what gets rebuilt and what you can live with
- Sends you the Friday report from week one

comparison:
eyebrow: The maths
headline: What you're comparing it against
fractionalLabel: Fractional CTO
fulltimeLabel: Full-time CTO
rows:
- label: Cost
fractional: A fraction of a full-time hire
fulltime: Full salary, benefits, and equity
- label: Equity
fractional: None
fulltime: Typically required
- label: Time to start
fractional: Days
fulltime: Months of hiring
- label: If it isn't working
fractional: You stop. No severance, no re-hire.
fulltime: Severance and a re-hire
- label: Best when
fractional: Pre-Series A, or stabilising after a bad build
fulltime: Post-Series A, with a team to own

steps:
eyebrow: How it starts
headline: Four weeks from first email to a plan you can act on
intro: You don't sign a retainer to find out what's wrong. The assessment comes first.
list:
- title: Code review first
body: "You send access. You get a written assessment: what's solid, what's fragile, what's going to cost you."
- title: A 30-minute call
body: We walk you through the assessment in plain English. Most founders take the document to their board whether or not they hire us.
- title: Week one on the ground
body: Your fractional CTO joins standups, reads the backlog, and sends the first Friday report.
- title: Month one plan
body: What gets rebuilt, what gets left alone, what it costs, in what order. You own the code and the plan.

clients:
eyebrow: Clients + case studies
headline: Clients stay for years, not quarters.
stats:
# `derived: tenure` is what makes the template compute the value from
# site.Params.foundingYear. Keyed on its own field, not on the label -
# a label-keyed branch falls back silently when someone rewords it.
- derived: tenure
label: Years of industry experience
- value: "5"
label: Years of average client relationship
- value: 8+
label: Years of average developer experience
cases:
- name: Agent Inbox
body: Hired us to build their real-estate communication platform, get it market-ready, and grow the team around it.
tech: [Rails, React, Node.js]
url: /clients/agent-inbox/
- name: Open Apply
body: Hired us to debug their school-admissions product, optimize the user experience, and streamline internal project management.
tech: [Rails, Hotwire, Tailwind]
url: /clients/open-apply/
- name: Mobile Coach
body: Hired us to support and extend the products around their enterprise chatbot platform, and to scale their R&D and engineering teams.
tech: [Rails, AWS, PostgreSQL]
url: /clients/mobile-coach/

testimonial:
eyebrow: From a client
# VERBATIM tail of the canon description in data/testimonials.yaml (Bruno
# Wozniak). The design blueprint had smoothed it to "They were detailed and
# precise, helping us find problems..." - a real person's words are not copy
# to tighten. Asserted by test/unit/next_rail_test.rb against the yaml.
quote: Their team was also detailed and precise, helping us to find problems before they appear. We knew exactly what was happening and where we were going the whole time.
attribution: Bruno Wozniak · Director of Engineering, PubNative
sourceName: verified Clutch review
sourceUrl: https://clutch.co/profile/jetthoughts
proof:
- "[**4.8**/5.0 on Clutch](https://clutch.co/profile/jetthoughts)"
- "**5-year** average client relationship"

closing:
headline: Still deciding?
body: The consultation is free. The assessment is yours either way.
cta:
name: Book a free consultation
url: /free-consultation/

footer:
columns:
- heading: Services
links:
- name: Fractional CTO
url: /services/fractional-cto/
- name: Fractional Product Management
url: /services/fractional-product-management/
- name: App/Web Development
url: /services/app-web-development/
- name: Talent Recruiting & Training
url: /services/talent-recruiting-training/
- heading: Proof
links:
- name: Clients
url: /clients/
- name: Use Cases
url: /use-cases/
- name: Friday report
url: /friday-report/
- heading: Learn
links:
- name: Blog
url: /blog/
- name: Course
url: /course/tech-for-non-technical-founders-2026/
- name: About us
url: /about-us/
---
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ zero FL-Builder export files remain in any slice.
| friday-report | `page/friday-report.html` | pages/friday-report.css | new 2026-08-20 | born semantic (no FL modules; one `#fl-main-content` specificity override against legacy-theme-skin) |
| pagination | `list.html` (second bundle) | — (pagination.css only) | 0.2K → 0.2K | no-FL |
| next-pilot | `layouts/next/single.html` (repo root, NOT in theme) | pages/next-pilot.css | new 2026-08-21 | v2 clean-slate rail (ADR-0006): css-variables + navigation.css + footer.css only — no style.css/586/base-4/legacy-theme-skin. Serves `/next/*` staging pages |
| next-rescue-room | `layouts/next/landing.html` (repo root, NOT in theme) | pages/next-rescue-room.css | new 2026-08-21 | Design-register pilot A (10.01). The register skin is the bundle's ONLY file — not even css-variables — because the pilot renders its own nav and footer and must be judged on its own surface. Serves `/next/pilots/rescue-room/*`. A new register adds a row here, a skin file, and one line to the `$skins` dict |

¹ careers was a byte-identical verbatim move (3086-layout2 had zero dead
nodes); the small growth vs 07-12 is upstream content drift (course v2 merge
Expand Down
Loading
Loading