> ## Content Index
> Fetch the complete content index at: https://scottmallinson.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# The guard that only worked because something else was broken
- URL: https://scottmallinson.com/the-guard-that-only-worked-because-something-else-was-broken/
- Published: 2026-10-05T08:17:00.000Z
- Updated: 2026-10-05T08:17:00.000Z
- Description: Two defects were cancelling each other out. Fixing the real one on its own would have woken a dormant guard with the wrong logic and made the page worse.
- Author: Scott Mallinson
- Tags: Engineering, Debugging, Frontend, Tooling

Every post on this site was shipping a broken `og:image` and a malformed BreadcrumbList. It had been for a long time. Nothing in the build complained, nothing in the logs, and the pages looked completely normal to anyone reading them.

The cause was one line of template logic that does something subtler than it looks. In Ghost's Handlebars themes, `{{#is "post"}}` asks whether the current route is a post. It doesn't open the post's data scope. So inside that block, at the top level of the default layout, every post field resolves to an empty string. Not an error, not a build-time warning: an empty string, which is a perfectly valid thing for a template to render.

That's the ordinary version of this bug, and I could have written it up as [another instance of a system answering a question it wasn't equipped to answer](https://scottmallinson.com/a-passing-test-that-never-saw-the-page/). The interesting part is what I found when I went to fix it.

## Two defects holding hands

Elsewhere in the same template there was a guard, an `{{#unless}}` block whose job was to stop a duplicate `og:image` being emitted when a post already had one.

That guard had never fired. It looked correct, it had been reviewed, and it was dormant, because the field it tested was one of the fields resolving to an empty string. An empty value doesn't trip an `{{#unless}}` guard. It inverts it. The guard was reading "there is no image here" every single time and concluding there was nothing to prevent.

So the two defects were holding hands. The scope bug suppressed the fields. The suppressed fields kept the guard asleep. Fix the scope bug on its own, which is the obvious and correct fix, and the guard wakes up with the wrong logic and starts firing in cases it shouldn't, on a page that had been fine.

I'd have shipped exactly that if I had stopped at the root cause. The root cause was real, the fix was right, and the fix alone would have made the page worse.

## What this changes about fixing things

The rule I took from it is narrow and I think defensible: when you fix something that was silently producing a default, go and look at everything downstream that was reading that default. Not because the fix is wrong, but because those readers have been running against a value that was never real, and some of them will have been written to suit it without anyone noticing.

A conditional is the obvious case, because a conditional on an always-empty value has a branch that has never executed. There's no coverage of it, no bug report about it, and no reason for anyone to have looked at it closely.

This is uncomfortable in a way that a normal regression isn't. The usual mental model is that fixing a bug moves the system towards correct. Here, fixing one of two interacting bugs moves it away, and the only signal that this is happening is you going to check.

## Two smaller versions of the same shape

The same week produced two more cases where a platform declined to tell me something rather than refusing outright.

Ghost ignores `meta_title` when resolving the author archive title, while honouring `meta_description` set through the same Admin API route on the same object. One of the pair works, the other is silently dropped, and there's nothing at the API boundary to suggest a difference. The fix had to move into the theme, because the platform was never going to use the value I was giving it.

The theme validator, gscan, rejects the author-scoped block helper and dotted author paths outside `author.hbs`. That's reasonable. What's less obvious is that it rejects them inside Handlebars comments too, so an explanatory comment about why the markup couldn't live there failed CI in exactly the same way the live markup would have. The tool doesn't distinguish between code and a note about code.

Neither is a defect exactly. Both are cases where the system could say "I am not going to do what you asked", and doesn't.

## Where I have landed

I don't have a general technique here, and I'm wary of dressing one up. What I have is a checklist item that has now earned its place: after finding a value that was always empty, always the default, search for its readers before shipping the fix.

The thing I keep turning over is that the dormant guard passed every review it was ever in, including mine. It reads correctly. You can't tell by looking at it that it has never once run. The only way I found out was by fixing something else entirely and asking what that fix switched on.

Is there a way to find never-executed branches in a template layer, short of instrumenting the renderer? I couldn't find one, and it feels like the sort of thing that should exist.