Skip to main content
  1. Posts/

My diagrams rendered on refresh and vanished on click. The head never loaded.

··1010 words·5 mins·
Nick Liu
Author
Nick Liu
Building infrastructure for Facebook Feed Ranking at Meta. Previously at Walmart, Twitter, AWS, and eBay. MS in Computer Science at Georgia Tech.
Table of Contents
A reader clicking from my homepage to a post with a diagram got a block of raw mermaid source. The same reader pasting that post's URL directly got a rendered diagram. Same page, same build, same browser. The difference was the click, and the bug had been live on this site for months across every diagram, every math formula, and every chart, because I had only ever tested pages by loading them directly.
Tested with Blowfish 2.10x · htmx 2.0.10

Two features, both reasonable, one collision
#

This site has htmx’s hx-boost on the body: internal navigation swaps page content in place instead of doing full page loads, which keeps transitions smooth. Separately, the Blowfish theme is smart about heavy libraries: mermaid, KaTeX, and Chart.js bundles are only included on pages that use them, injected into the <head> of exactly those pages.

Each feature is sensible alone. Together they have a hole in the middle:

hx-boost navigation

htmx fetches the new page

swaps the BODY only

the new head, and its
bundle script, never execute

typeof mermaid is undefined,
re-init guard silently skips

raw diagram source
on the page

direct load / refresh

browser parses full HTML

head scripts execute,
mermaid bundle loads

diagram renders

hx-boost navigation

htmx fetches the new page

swaps the BODY only

the new head, and its
bundle script, never execute

typeof mermaid is undefined,
re-init guard silently skips

raw diagram source
on the page

direct load / refresh

browser parses full HTML

head scripts execute,
mermaid bundle loads

diagram renders

hx-boost replaces the body and updates the title; it does not merge the incoming head. So when you navigate from a page without diagrams to a page with them, the one script tag that would have loaded the mermaid bundle is in the part of the document that gets thrown away. My re-render code even had a guard, if (typeof mermaid !== 'undefined'), which is exactly the kind of guard that turns a loud failure into a silent one. It skipped, politely, every time.

Direct loads worked because the full document parses normally. Which is why months of my own testing missed it: I test pages by URL. Readers arrive by link.

The fix: steal the bundle URL from the response you already have
#

The swapped-in page’s HTML passes through htmx’s hands, head included, before the body is extracted. The fix reads the bundle URL out of that response and injects the script once:

function ensureVendorThenRender(evt, srcPattern, isReady, render) {
  if (isReady()) { render(); return; }
  var xhr = evt && evt.detail && evt.detail.xhr;
  var html = xhr && xhr.responseText;
  var match = html && html.match(srcPattern);
  if (!match || injectedVendors[match[0]]) return;
  injectedVendors[match[0]] = true;
  var s = document.createElement("script");
  s.src = match[0];
  s.onload = render;
  document.head.appendChild(s);
}

Called from the htmx:afterSettle handler with a pattern like /\/js\/mermaid\.bundle\.[^"']+\.js/, once per library. Fingerprinted filenames come along for free since the pattern matches whatever hash the current build produced.

Charts needed a second layer. The theme’s chart shortcode wraps its new Chart(...) call in a DOMContentLoaded listener, and after an htmx swap that event is ancient history; the listener registers and waits forever. So the chart path re-inserts each inline chart script with addEventListener temporarily patched to invoke DOMContentLoaded callbacks immediately, plus a Chart.getChart() check so nothing draws twice. It is not elegant. It is honest about the constraint: the shortcode’s markup is the theme’s, and the fix has to meet it where it is.

If you want the pattern’s shape as a takeaway: conditionally loaded assets and partial navigation are individually fine and jointly broken, and neither feature’s documentation will warn you about the other. Any site combining per-page bundles with hx-boost, Turbo, or SPA-style routing has this class of bug available to it.

The tests were their own debugging story
#

I added three Playwright regression tests, each walking the failing path on purpose: land on a listing page without the library, click through to a post with it, assert the rendered artifact exists (pre.mermaid svg, .katex, a live Chart.getChart() instance). Two traps surfaced on the way, and they cost more time than the fix itself.

First, the clicks kept landing on the wrong page. Blowfish’s article cards stretch the post link across the whole card and stack tag links on top of it, and Playwright’s coordinate-based click aimed at the card’s center, which happened to be a tag chip. The katex test spent several runs waiting for math on /tags/georgia-tech/. The cure is locator.dispatchEvent('click'), which delivers the event to the exact anchor regardless of what floats above it, plus a waitForURL assertion so a wrong destination fails fast with a readable error instead of a timeout.

Second, my shell habit was eating the exit code. npx playwright test | tail reports tail’s status, which is always zero, and a failing suite sailed through my terminal looking green twice before I noticed. Redirect to a file and check the real exit code. CI was never fooled, because CI does not pipe; only my eyeballs were.

The affected pages, for the record, included every post in my ML math series and the diagram in the hooks post. All fixed by the same three functions, all guarded by the same three tests.

Lessons
#

  • Per-page asset loading plus body-swap navigation is a bug class, not a bug. If your head is conditional and your navigation is partial, budget for the collision.
  • A typeof x !== 'undefined' guard converts a crash you would notice into a skip you will not. Guards need a fallback path, not just an exit.
  • Test the click path, not only the URL path. Direct loads exercise the document pipeline; in-site navigation exercises a completely different one.
  • In tests, dispatch events to the element you mean. Coordinate clicks trust the visual stack, and the visual stack lies on layered layouts.
  • Never pipe a test runner’s output when you care about its exit code.

References
#

Related

Blowfish supports four analytics providers. Cloudflare Web Analytics isn't one.

··862 words·5 mins
For six months I assumed nobody could tell whether anyone read this blog, because I had never added analytics. Wiring up Cloudflare Web Analytics by hand taught me two things: the obvious place to paste the snippet would have shipped my Playwright suite's page views into the dashboard, and the dashboard had been quietly counting my visitors for two months anyway. 🧪 Tested with Hugo 0.163.3 · Blowfish 2.104 Publishing into the void # The site’s hugo.toml had a googleAnalytics line commented out since roughly the first commit. I never uncommented it. GA4 wants a cookie disclosure, ships a chunky client, and ad blockers eat it anyway, which felt like a lot of ceremony for a personal blog whose one open question was “does anybody visit.”

Five wrong answers in one day. One nearly deleted 10 GB of coursework.

··1903 words·9 mins
`mdls kMDItemLastUsedDate` returned `(null)` for Microsoft Word. I read the null as "never opened" and put Office on a removal list: 10.1 GB, four apps. One last check saved me. My home directory held 150 Office documents, a conference presentation edited two weeks earlier, and a PowerPoint lock file, which only exists while the file is open. The proof that the null was misleading had been sitting in my own diagnostic report for an hour. That was one of five. In a single day of hardening this machine, five different tools told me things that were not true. None of the answers looked like an error. Each one arrived as a clean, confident finding, and under each one a check had quietly failed or asked the wrong question. All five had the same shape underneath. Once I could name the shape, I stopped falling for it.

My commands vanished with exit 0. The culprit was a file named env.

··1441 words·7 mins
`env -u VAR command` did nothing. Exit code 0, no output, no error. A different command, same thing. A different variable, same thing. Any invocation that started with `env` just quietly evaporated. What finally made the problem visible was a `git init` that reported success while creating no `.git` directory at all. That was a year ago. The case closed last month, and the culprit was not uv, not some third-party installer, not anything exotic. It was this repo’s own bootstrap script. The thing that caught it was the regression test I had written for the original incident.