Self-hosting

Grep proves presence, rendering proves correctness

4 min read

I once shipped a website redesign to production that was unreadable. Not subtly off — headings the same colour as the background, sections stacked into columns, the lot. And I had verified it at every step.

I had verified the wrong thing. Every check I ran confirmed that my CSS was present in the stylesheet being served. Not one of them confirmed that the page looked right. Those are different claims, and the gap between them is where this class of bug lives.

The mistake, precisely

My checks all looked like this:

curl -s https://example.com/style.css | grep -c "background"

Non-zero, so the rule is there, so it applies. That last step is the false one. A CSS rule existing in a file says nothing about whether it wins the cascade.

The theme I was building on top of injects an inline <style> block after any external stylesheet, and its rules were both more specific and later in document order. So it won twice over. The only declarations of mine that survived were the ones where I had happened to write !important — which is exactly why the page was partially correct, the most misleading outcome available.

“The code is present” is not “the behaviour is correct.” This applies far past CSS: a config key in a file that a later include overrides, a firewall rule after a blanket accept, an environment variable set in a shell the service does not read. Any time your verification inspects an input rather than observing an output, you are asserting something you have not tested.

The fix: attack the source of specificity, not each symptom

My first instinct was to out-specify the theme rule by rule. That is an arms race you lose, because you only patch the rules you noticed — every component you did not look at stays broken.

The theme’s inline block sets its colours from its own CSS custom properties. So the fix was one block that redefines those variables rather than the rules that consume them:

:root {
  --theme-color-2: var(--my-heading-color) !important;
  --theme-color-4: var(--my-background-color) !important;
  /* ...one line per variable the theme defines */
}

Every rule the theme writes, including for components I had never styled or even seen, now resolves to my palette. One small block replaced a growing pile of overrides, and it fixed pages I had not tested yet.

This is the general shape of a root-cause fix. If you are writing the same override in many places, you are patching downstream of the actual cause. Find the single thing they all read from.

Verify by rendering

The correction to my process was to stop reading and start looking. Headless Chromium takes a screenshot from the command line, so there is no excuse:

chromium --headless --disable-gpu --no-sandbox \
  --window-size=1400,1600 --virtual-time-budget=12000 \
  --screenshot=/tmp/shot.png "https://example.com/?shot=$RANDOM"

Then open the image. Generating it and not looking at it is the same bug I started with.

Two details in that command, both learned the hard way:

The cache-busting query string is not optional. My host caches HTML for days. Without ?shot=$RANDOM I would have been screenshotting the previous deploy and drawing confident conclusions from it.

The time budget matters. Headless Chromium captures whenever the budget expires, whether the page finished or not. Too short and you screenshot a half-loaded page, then go hunting for a rendering bug that does not exist.

Two false positives that will send you chasing ghosts

Rendering in a container introduces its own lies. Both of these cost me time diagnosing things that were perfectly fine in production.

Missing fonts look like broken markup. My screenshots showed every emoji as an empty box. I read that as broken icon markup. The container simply had no colour emoji font installed. One package fixed “the bug”.

A slow asset looks like a missing asset. An image showed up blank in the screenshot, so I assumed a bad path. It was a 6 MB file that had not finished downloading inside the time budget. Before concluding a resource is broken, ask the server directly:

curl -o /dev/null -w '%{http_code} %{size_download}\n' https://example.com/image.png

A 200 with a sane byte count means the asset is fine and your capture environment is the problem.

The rule I kept

For anything with a visual or user-facing output, the verification step has to observe that output. Inspecting the inputs — the file, the config, the rule — tells you what you intended, and the whole reason bugs exist is that intent and behaviour diverge.

Grep proves presence. Only rendering proves correctness.

Scroll to Top