Writing · Learning Log

Silent Failures I Found While Rebuilding This Site

7 minute read

I rebuilt this site over the last few weeks — new design system, new information architecture, new templates. What I did not expect was that the most useful part of the exercise would have nothing to do with design.

Rebuilding forced me to actually look at every page. And once I did, I found seven bugs. Not one of them had ever produced an error. jekyll build was green the whole time. No console exception, no failed deploy, no broken-link warning. The site was quietly wrong, and had been for months.

That pattern is familiar. When I built a Corrective RAG pipeline last year, the lesson was that hallucinations are usually retrieval failures — the system proceeds confidently because nothing told it to stop. A static site has the same failure mode, just with less glamour attached.


The publications page listed zero publications

This is the one that stung.

My /publications/ page had a working layout, a heading, and no publications. The generator script that turns a spreadsheet into markdown files was writing this into every entry’s front matter:

collection: manuscripts

collection is a Jekyll concept — it has to be the name of the collection, which is publications. The books / manuscripts / conferences grouping is a completely separate category field, which the page template reads to decide which section an entry belongs under.

So every generated publication had a collection that meant nothing and no category at all. The template looped over the known categories, found nothing matching, and moved on. An empty page is a perfectly valid page.

Two things came out of this:

  1. The script now writes collection: publications plus a real category read from the source data.
  2. The template lists anything uncategorised under Other instead of skipping it.

The second fix matters more than the first. The first one fixes today’s bug; the second one makes the same class of bug visible next time instead of silent.

If your rendering code can drop a record, it will eventually drop a record. Make the fallback loud.


Every job on my CV rendered as a table

My CV had lines like this:

**Askturing.ai** \| *June 2025 - Present*

I had escaped the pipe, which is exactly what you do when you want a literal | character. Kramdown looked at a line containing a pipe, decided it was looking at a GitHub-flavoured markdown table, and rendered each employer as its own one-row <table>.

The escape was correct in isolation and wrong in context. There is no error here to catch — it is markdown doing precisely what markdown is specified to do, on input that looked fine to me.


The CV had eight <h1> elements

The same file used Setext-style headings:

Work experience
======

Those ====== underlines produce <h1>, not <h2>. Under a layout that already emits the page title as an <h1>, the CV shipped with eight of them.

Nothing breaks. The page looks right. But document outline is what screen readers navigate by, and it is one of the few structural signals search engines still read literally. Every page on the site now has exactly one <h1>, and that is now something I check rather than assume.


Two buttons that went nowhere

A “View Markdown CV” button pointed at `` — the site root. Click it, land on the homepage. It looked like a working button because it was a working link; it just pointed at the wrong place.

A second button offered a PDF download at a path where no PDF had ever existed. The fix was not to add the file — it was to render the button only when the file is actually present. Now it appears on its own the day I drop a PDF in, and cannot 404 in the meantime.

A link that 404s tells you something is wrong. A link that silently goes to the wrong page tells you nothing.


A theme toggle that referenced an undefined variable

Buried in the inherited JavaScript:

return (userPref && userPref("(prefers-color-scheme: dark)").matches)
  ? "dark" : "light";

userPref is never defined anywhere in that file. Referencing an undeclared identifier throws a ReferenceError — the && guard does not save you, because evaluating the left-hand side is itself the thing that throws. Ten lines below, the same file correctly uses window.matchMedia for the same purpose.

This never surfaced because the function was only reachable on one code path I had not exercised. Dead-ish code is still code, and it will eventually stop being dead.


The accent colour I picked for body links measured 4.04:1 against the page background. WCAG AA wants 4.5:1 for body text. Close enough to look completely fine, far enough to fail.

Worse, the visited-link colour was lightening the accent against a light background, landing at 2.93:1 — a state that could never reach AA no matter how I tuned it.

Fixes: drop the accent’s lightness by 3% (#C4571F#B24F1C, same hue and saturation) to reach 4.75:1, and darken the visited colour instead of lightening it, which takes it to 7.88:1.

The lesson is that contrast is arithmetic, not taste. I cannot eyeball a ratio, and neither can you. It takes ten seconds to measure and I had spent months not measuring.


What these have in common

Sorting through them afterwards, the seven bugs fall into two buckets:

Wrong-but-valid input. The escaped pipe, the Setext headings, the collection: manuscripts. In each case I wrote something the tool accepted and interpreted differently than I meant. No validator will catch these, because there is nothing invalid about them.

Never-executed paths. The undefined variable, the button pointing at the site root, the missing PDF. Code and links that nobody had walked in the specific way needed to notice.

Neither bucket produces an error, which means neither bucket is caught by “does it build?” — the only check I was actually running.

What I do differently now:

  • Check the output, not the build. After every build I now crawl the rendered site: every route returns 200, every internal link resolves, every page has exactly one <h1>. That crawl is what found four of these seven.
  • Make fallbacks visible. An “Other” bucket beats a continue. A button that hides itself beats a button that 404s.
  • Measure the things that are numbers. Contrast ratios, heading counts, link counts. If it can be counted, do not eyeball it.

None of this is sophisticated. It is the static-site equivalent of the c-RAG decision node: put one cheap check between “I produced something” and “I shipped it,” and insist the system tell you when it does not know.

The full rebuild — design system, layouts, information architecture — is documented in the repo. But the redesign was the excuse. Looking at every page was the actual work.