Phase 1a.1: promote the Rescue Room tokens site-wide (zero visual delta) - #503
Merged
Conversation
The light palette (ADR-0003, resolved 2026-08-20) lived in an rr- prefixed
:root block inside single-post.css, reachable only by the three blog
bundles. Promoted to foundations/css-variables.css, which loads inline
site-wide via the navigation bundle - that is what lets the other 17
bundles resolve these colours in 1a.2-1a.4.
Zero-delta by construction AND verified: values identical to the block they
replace, rr- names kept as aliases so every existing consumer resolves to
exactly the same colour. Computed values checked in the browser (aliases
resolve to the canonical tokens; pill colour, row border and feature
background all unchanged), then qtest: 53 screenshots compared, 0 failures,
0 baseline churn. Blog special-content and course suites green too - course
shares single-post.css, so the dedup had to leave it untouched.
This sequencing is the point of 1a.1: it turns the 1a.2 --color-primary
deletion into a value change rather than a rename, so that phase's diff is
about colour roles and nothing else.
Adds --ink-*, --line, --surface-*, --ruby-{700,100,050}; keeps the existing
--color-ruby / --color-obsidian* families untouched. rr- aliases are deleted
in 1a.4 once consumers move to the canonical names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
css-pipeline.md had no token layer at all - a real gap, since foundations/css-variables.css is the one file loaded inline in every bundle and therefore the only place a site-wide colour change can start. Now documents the file, its contents after 1a.1, and the zero-delta promotion pattern worth reusing: move tokens in their own commit with the old names kept as aliases, so the NEXT commit is a value change rather than a rename. Also records --color-primary and the #0066d6 rule as deprecations in progress, with retiring all 55 !importants as the phase success signal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
pftg
added a commit
that referenced
this pull request
Aug 20, 2026
…utes A Codex review found a P1 that made the whole SEO effort a no-op, plus a bug in my own generator. Both verified in source before fixing. 1. THE REPAIRS WERE NOT DURABLE. lib/sync/post.rb:39-45 re-pulls title and description from dev.to unless the frontmatter carries `seo_override: true`. Its own comment says "the 10-min sync cron clobbers any locally-edited SEO snippet". None of the 141 files I edited had the flag, so every rebuilt description and the hand-written SimpleCov title would have been reverted within ten minutes of merge - silently, with the commits still in history looking like they worked. All 140 dev.to-backed files now set it, verified by script: 140 with the flag, 0 missing. The SimpleCov post gets it too, since its title and description were both hand-written. 2. MY GENERATOR ATE UNDERSCORES. clean() ran gsub(/[*_`]/, "") to strip markdown emphasis, which also stripped underscores inside identifiers. `stringify_keys` shipped as "stringifykeys" - destroying the exact keyword that post targets, in the description Google reads. Emphasis is now stripped only when the markers actually wrap something, and inline code is unwrapped rather than deleted. Blast radius measured rather than assumed: exactly 1 of 141 descriptions contained an underscore, so one post was mangled, not many. All 141 were restored to their pre-commit state and regenerated from source with the corrected cleaner - 140 rebuilt, 93 left for a human. Two factual corrections in the new posts, both verified in the gems: - R2 claimed langchainrb gives token counts "on every response". The Hugging Face, llama.cpp and Replicate response classes never override those methods, so they inherit base_response and raise NotImplementedError. Now scoped to the mainstream adapters, with the exceptions named. - R3 said a retried POST could duplicate a row write or an email. It cannot. chat.rb:233 runs handle_tool_calls only after a response returns, and the retry middleware wraps the HTTP request below that - so a lost response can double-charge the completion, but the first attempt never reached the tool. Duplicate side effects come from retrying the job around the call. The post now says so. Local macOS suite (not CI): 34 runs, 87 assertions, 0 failures, and 53 screenshots compared with no failures. That also answers the CI Screenshot Tests failure - footer and CTA regions on homepage/services do not reproduce locally, and the linux baselines date from #494 with design work landed since (#503 tokens, #508 palette). Drift, not this diff. bin/hugo-build 8/8.
pftg
added a commit
that referenced
this pull request
Aug 20, 2026
… entries (#520) * fix(seo): both page-1 posts were serving Google a truncated SERP entry Two posts rank page-one and take almost no clicks: `solid queue vs sidekiq` at position 5.2 with 0.86% CTR (116 impressions, 1 click), and `simplecov` at 10.3 with 0.87% (115 impressions, 1 click). Expected CTR at position 5 is roughly 6%. I assumed by analogy to the Kamal post that this was a weak title, then read the RENDERED output instead of the source, and the real defect was mechanical. `layouts/partials/seo/enhanced-meta-tags.html` truncates a blog title to 45 chars (:14), appends " | JetThoughts Blog", then truncates the whole string at 60 with an ellipsis (:8, :22). A title over 45 therefore gets cut twice and lands in the SERP trailing an ellipsis AFTER the brand: before: Solid Queue vs Sidekiq: Complete Comparison | JetThoughts… after: Solid Queue vs Sidekiq: When Each Wins | JetThoughts Blog before: How we configure Simplecov for our Ruby on | JetThoughts… after: How to Configure SimpleCov in Rails | JetThoughts Blog That is also WHY the pipeline's "title <= 45 chars" rule exists - a rule I had been following without knowing the mechanism. Descriptions were broken independently. Solid Queue's ran 162 chars and lost "included." to the 160 cap. SimpleCov's ended in a literal "..." in the source file - a dev.to import artifact, since dev.to truncates descriptions near 100 chars and the importer carried the ellipsis into our meta tag verbatim. Both descriptions now answer the query instead of listing the contents, and both render whole. Content untouched. Solid Queue is 2,161 words and did not need it; SimpleCov is 561 words and DOES look thin against SimpleCov's own docs at position 10 - flagged, not fixed here, because that is a rewrite rather than a snippet fix. Also in this commit: the 2026-08-20 P0 gate override recorded in 20.09, so a later session reads the content sprint as a deliberate call rather than a gate nobody checked. Verified in rendered HTML, not source: bin/hugo-build 8/8 validators, zero ellipsis in either title or description. * fix(seo): stop truncating post titles twice - 454 of 614 shipped an ellipsis Paul authorised dropping the brand suffix if it helped. It does, and the underlying bug was arithmetic the template contradicted itself on. `enhanced-meta-tags.html` cut a post title to 45, appended " | JetThoughts Blog" (19 chars) for 64, then cut the result again at 60. 45 + 19 = 64 > 60 ALWAYS, so any title of 42+ characters was truncated twice and reached the SERP trailing an ellipsis after the brand name: How we configure Simplecov for our Ruby on | JetThoughts… Solid Queue vs Sidekiq: Complete Comparison | JetThoughts… Insights from Zapier's CTO on Managing Remote… The template's own two constants could never both hold. Its effective safe length was 41 chars, not the 45 the code implied - which is also why the blog pipeline's "title <= 45" rule exists, a rule I had been following without knowing the mechanism. Measured before the change: 454 of 614 posts (74%) carried titles over 45 chars. After: of 690 rendered post titles, exactly ONE still contains an ellipsis - "Why AI Hasn't Blown Our Minds…Yet", where it is deliberate punctuation. Fix is structural rather than numeric: truncate EXACTLY once. Post titles take the full 60 with no brand suffix; the second length guard moved inside the non-blog branch, where the suffix is still appended and still needs it. Google appends the site name itself when it wants one. Diagnosis note, because the wrong answer was convincing: reading the source suggested the blog branch already handled this. Rendered output disagreed. Two probe markers in the template proved the second truncate was firing on top of the first - the branch was right and ran twice. Source-reading would have shipped a no-op. Gates: bin/hugo-build 8/8 validators. `bin/qtest --changed` reports no visual-affecting changes, correctly - title and meta tags move no pixels. og:title and twitter:title verified to follow the same clean string. * fix(seo): rebuild 141 meta descriptions the dev.to import left mid-sentence 234 of 615 posts served Google a description ending in a bare '...'. The cause is structural: 529 posts (86% of the blog) came from dev.to, which truncates its description near 100 chars, and the importer copied that ellipsis into our frontmatter verbatim. The full sentence is almost always still in the post body, so these are rebuilt from the body's first real prose - headings, code fences, images and list markers skipped. STRICT on purpose: a description is only written when whole sentences fit the 158-char budget. My first attempt fell back to a word-boundary cut and I inspected the output before shipping it - roughly a quarter ended in dangling phrases: "...and create not just a good headline, but a catchy one? No matter what your content type is, and if you're either writing a small" "...to get the results you envisioned for your" That is worse than the ellipsis it replaced, because nothing signals to the reader that the text was cut. So the rule is now sentence-boundary or skip. Cost of the stricter rule: 141 rebuilt instead of 220, and 92 left for a human rather than auto-filled badly. Verified mechanically across all 141 written: zero end in '...', zero end without terminal punctuation. Remaining 92 need a written description - they are posts whose opening prose has no complete sentence inside the budget. Not attempted here. bin/hugo-build: 8/8 validators. * feat(content): R2 - RubyLLM vs langchainrb for Rails Queue row R2. Researched by unpacking both gems, not from memory: ruby_llm 1.16.0, langchainrb 0.19.5, langchainrb_rails 0.1.12. The thesis came out of the file lists rather than a feature survey. One gem ships chat/agent/embedding/cost/model-registry; the other ships vectorsearch/chunker/loader/output_parsers/evals. They are not competing implementations of one job, so the decision is "do you retrieve from your own documents", not "which is better". That also satisfies the plan's constraint that R2 must LINK the LangChain guides rather than cannibalize them. Three critics plus a cold-eyes gate ran. What they caught: - PROVIDER COUNT WAS WRONG. I wrote 10 for ruby_llm; it registers 13 (lib/ruby_llm.rb:104-116). My `ls providers/ | head -20` truncated the listing and I read the cut-off result as complete - the same error as an undersized grep window earlier in the session. The correct number is better for the post: 13 vs 13 is a tie, so the row settles nothing, which is the point. The old draft warned against choosing on provider count while printing a wrong one. - THE STABILITY ROW WAS A CHEAP SHOT. "past 1.0 vs pre-1.0" implied quiet upgrades. ruby_llm ships upgrade_to_v1_7/v1_9/v1_10/v1_14 generators and an acts_as_legacy shim - four schema migrations inside minor releases. Now symmetric: budget for migrations on either. - I DISMISSED A GEM I HAD NOT OPENED. The draft waved at langchainrb_rails 0.1.12 with "that version tells you what to expect". Opening it proved me wrong: four generators (pgvector, pinecone, chroma, prompt), an ActiveRecord hook, a Railtie. The truth argues the thesis better than the smirk did - ruby_llm's generators scaffold a conversation, langchainrb_rails' scaffold a vector store. - COST CLAIM OVERSTATED. langchainrb does ship prompt/completion/total token counts; what it lacks is price and the cache/thinking split. "You supply the price table" replaces "you fly blind". - FIVE BANNED DEFINITIONAL CONSTRUCTIONS, one slogany flip, one negative parallelism, one "teams add last and wish they had added first" chiasmus. All removed; sweep now returns zero. Rejected three critic rewrites that would have fabricated evidence - an invented client bill going "$340 to $1,900", a count of "two of the last four apps", an OpenAI format change "found in production". None happened. Used the one real published incident instead (nine schemas, 1549 green tests, VCR matching on method and URI only) and checked my citation against the source post, which caught me inflating it to "nine agent pipelines" when it was nine schemas in one pipeline. Model id in the sample is claude-sonnet-4-5, not gpt-4o. gpt-4o still resolves in the registry, but the gem's own README uses a current model and this post argues that models get retired underneath you. Gates: bin/hugo-build 8/8. check-post-visuals back at floor 72 (the decision tree, which routes rather than restating the table). Mermaid pre-rendered, 499.9px viewBox = 9.36px at 390px against a 9px floor. Rendered scroll gate at 390px: zero console errors, no page overflow. Cover generated and verified in rendered output - cold-eyes caught that the missing cover.png was falling back SILENTLY to the site default og-default.jpg with no build error. og:image now resolves to the real file. Dropped the Raw HTTP column from the table: at 390px it rendered 520px wide in a 464px container with overflow-x visible, so the whole column was clipped and unreachable. Its cells all read "you write it" and it has its own section. Table now fits exactly at 464. Noted, not fixed: that clipping is a theme-level issue, not specific to this post - wide tables have no scroll container. Needs its own change plus a visual regression run. * feat(content): R3 rescoped - what RubyLLM retries, and for how long The queue row R3 read "rate limits, token budgets, retries, streaming into Turbo". Audited against content/blog/ before drafting and three of those four were already owned by posts that shipped AFTER the row was groomed: token budgets -> cost-optimization-llm-applications-token-management streaming -> fibers-async-ruby-llm-streaming-rails rate limits -> same post, "Rate limiting the upstream calls" Writing it as specified would have cannibalised two posts - the same collision that killed R9 and redirected the Kamal work. Only retries were unclaimed, so the post is retry semantics. Verdict recorded in 20.09 and the row retired. Researched by unpacking ruby_llm 1.16.0 and faraday-retry 2.4.0. I was wrong twice before reading them, which is the reason the post exists: 1. Guessed the 0.1s interval was too fast for a rate limit. Wrong - faraday-retry reads Retry-After AND the rate-limit reset header and takes the larger, so a 429 gets the provider's number. 2. Then assumed a sane ceiling on that wait. Wrong - max_interval defaults to Float::MAX (middleware.rb:55) and ruby_llm never sets it, so the guard that would abandon an over-long wait never fires. The other half is connection.rb:111 adding :post back to Faraday's IDEMPOTENT_METHODS, which excludes POST by design. Correct for chat completions, dangerous for a tool call with a side effect. Fact-checker verified all eight core claims against source and caught: - retries vs attempts off-by-one (max: 3 is three RETRIES, four attempts) - request_timeout IS exposed and defaults to 300s. My draft said you cannot configure your way out; the real worst case stacks four attempts x 300s against up to three Retry-After waits, so well over half an hour rather than the five minutes one Retry-After suggests - "would never retry anything at all" was false - a GET for the model list still retries. Narrowed to "a single completion" - Faraday::RetriableResponse is listed but inert, because retry_statuses is never set. Source-true, behaviour-false - now labelled dormant - the OpenAI source link implied provider header behaviour I had not verified. Replaced with a note telling the reader to check their own provider's docs Cold-eyes caught the worst one: I cited our own pool-exhaustion post as evidence for what happens when a model call is NOT in a job. That incident happened INSIDE a job. It now reads "a job is not a free pass either", which also resolves a contradiction with "inside a Sidekiq job that is fine" three paragraphs earlier. It also pulled the model- retirement claim back to what the source post actually supports, matching the correction shipped in #509. Gates: bin/hugo-build 8/8. check-post-visuals at floor 72 (decision diagram, 257px viewBox - 12px text renders true-size at 390px, clear of the 9px floor). Rendered scroll gate at 390px: zero console errors, no page overflow, table fits at 464. og:image resolves to the post's own cover derivative, verified in built HTML rather than assumed. Ran two critics rather than four - the fact-checker and cold-eyes, which between them caught every shipping defect on R2 while the style critics caught style. Stating the reduction rather than implying full coverage. * style(content): plainer English in R2 and R3, and drop the padded source lists Paul asked for plainer English and less AI-feel. The voice guide's first gate is exactly this - a sentence the reader has to decode has already failed, no matter how it scores on the mechanical checks. The sentences that needed it were the ones carrying a metaphor where a plain word would do, or two ideas welded together: "Both gems brought their own centre of gravity into Rails" -> "Each gem brought the thing it is good at into Rails" "Providers are a tie at thirteen each, so that row settles nothing" -> "Both ship thirteen providers, so that row will not help you choose" "Neither gem promises you a quiet upgrade" -> "Neither one gives you upgrades for free" "That failure is quieter than it sounds" -> "That kind of change is easy to miss" "A second gem constructing its own requests doubles the surface" -> "twice as many places for that to hide" "What you keep writing yourself is the boring layer" -> "What you write yourself is the dull but necessary part" "Source-true, behaviour-inert" -> "It is in the code, but nothing reaches it" Also removed both Sources blocks. Paul flagged them as redundant and the slop critic had said the same thing earlier - six links, every one a first-party vendor page the reader can find from the gem name. A trailing bibliography reads as generated; thoughtbot links where the claim lives. Each post now ends with one line naming the two things actually read and telling the reader to check their own installed versions. Facts unchanged: 13/13 providers, the 1549-test incident, Float::MAX, the 0.1/0.2/0.4 schedule, every version number. bin/hugo-build 8/8. Tension worth noting for later: #510 standardised 20 posts onto a "## Sources" heading. That was about naming lists consistently, not about whether they should exist. This commit is the other half - do not pad one with generic vendor links just because the heading is there. * docs(voice): the plain-English rule needed a tech-stream example Section 0 had one worked example, from a first-person LinkedIn post, where the defects were borrowed drama and inverted causality. Today's pass on two tech posts hit a different family, and it is the one that recurs in the Rails stream: an abstraction standing where a plain word fits. Added the seven-row table of what shipped vs what it became - 'centre of gravity', 'settles nothing', 'quiet upgrade', 'doubles the surface', 'Source-true, behaviour-inert'. Every one of those passed banned-word, em-dash and slop checks. They fail the only test that matters: the reader has to translate before they can use the sentence. Named the tell so it is greppable in review: a noun phrase doing a verb's job. 'Centre of gravity', 'the surface', 'the boring layer' all gesture at a shape instead of saying what happens. The fix is the concrete verb, after which the metaphor is unnecessary. Also added a 'padded source list' row to the structural-patterns table. Six first-party vendor links shipped on a draft today; Paul flagged them as redundant and a slop critic had called the shape a generated-text tell earlier in the same session. Worth recording because #510 had just standardised 20 posts onto a '## Sources' heading - making a section cheap to add is what makes padding it easy, so the naming convention and this rule have to travel together. * fix(seo): the description repairs would have been reverted in ten minutes A Codex review found a P1 that made the whole SEO effort a no-op, plus a bug in my own generator. Both verified in source before fixing. 1. THE REPAIRS WERE NOT DURABLE. lib/sync/post.rb:39-45 re-pulls title and description from dev.to unless the frontmatter carries `seo_override: true`. Its own comment says "the 10-min sync cron clobbers any locally-edited SEO snippet". None of the 141 files I edited had the flag, so every rebuilt description and the hand-written SimpleCov title would have been reverted within ten minutes of merge - silently, with the commits still in history looking like they worked. All 140 dev.to-backed files now set it, verified by script: 140 with the flag, 0 missing. The SimpleCov post gets it too, since its title and description were both hand-written. 2. MY GENERATOR ATE UNDERSCORES. clean() ran gsub(/[*_`]/, "") to strip markdown emphasis, which also stripped underscores inside identifiers. `stringify_keys` shipped as "stringifykeys" - destroying the exact keyword that post targets, in the description Google reads. Emphasis is now stripped only when the markers actually wrap something, and inline code is unwrapped rather than deleted. Blast radius measured rather than assumed: exactly 1 of 141 descriptions contained an underscore, so one post was mangled, not many. All 141 were restored to their pre-commit state and regenerated from source with the corrected cleaner - 140 rebuilt, 93 left for a human. Two factual corrections in the new posts, both verified in the gems: - R2 claimed langchainrb gives token counts "on every response". The Hugging Face, llama.cpp and Replicate response classes never override those methods, so they inherit base_response and raise NotImplementedError. Now scoped to the mainstream adapters, with the exceptions named. - R3 said a retried POST could duplicate a row write or an email. It cannot. chat.rb:233 runs handle_tool_calls only after a response returns, and the retry middleware wraps the HTTP request below that - so a lost response can double-charge the completion, but the first attempt never reached the tool. Duplicate side effects come from retrying the job around the call. The post now says so. Local macOS suite (not CI): 34 runs, 87 assertions, 0 failures, and 53 screenshots compared with no failures. That also answers the CI Screenshot Tests failure - footer and CTA regions on homepage/services do not reproduce locally, and the linux baselines date from #494 with design work landed since (#503 tokens, #508 palette). Drift, not this diff. bin/hugo-build 8/8.
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.
First step of Phase 1a (20.02),
unblocked by the light palette decision in #502.
What
The light palette lived in an
rr--prefixed:rootblock insidesingle-post.css, reachable only by the three blog bundles. It moves tofoundations/css-variables.css, which loads inline site-wide via thenavigationbundle — that is what lets the other 17 bundles resolve thesecolours in 1a.2–1a.4.
Adds
--ink-*,--line,--surface-*,--ruby-{700,100,050}. The existing--color-rubyand--color-obsidian*families are untouched.Why it ships alone
Zero-delta by construction and verified. Values are identical to the
block they replace, and the
rr-names survive as aliases(
--rr-ink-900: var(--ink-900)), so every existing consumer resolves toexactly the same colour.
tokens; pill colour, row border and feature background all unchanged
bin/qtest --changed: 53 screenshots compared, 0 failures, 0 baseline churnsingle-post.css, so the dedup had to leave it untouched, and didThat is the point of sequencing it first: it turns the 1a.2
--color-primarydeletion (161
var()+ 52 literals) into a value change rather than arename, so that PR's diff is about colour roles and nothing else.
Also
.okf/architecture/css-pipeline.mdhad no token layer documented at all —a gap, since this file is the only place a site-wide colour change can start.
Now documents it plus the reusable zero-delta promotion pattern, and flags
--color-primary/ the#0066d6anchor rule as deprecations in progress.rr-aliases are deleted in 1a.4 once consumers move to the canonical names.🤖 Generated with Claude Code