SlowDen

Why 3 of our 16 blog posts had no analytics tag

By Liza · SlowDen · Published 12 August 2026

Three of our sixteen blog posts were about to ship with no analytics tag at all. Not a broken tag, not a wrong measurement ID — the snippet was simply not in the file. The pages rendered correctly, validated, carried their canonical link and their structured data, and would have returned a perfectly ordinary 200.

The cause is one line of reasonable behaviour: our page generator refuses to overwrite a file a human wrote. That refusal is correct. Its side effect is that the one page type written by a human is the one page type the head template cannot reach.

Nobody found this by reading a page. It was found by a build check that had never existed before, and it had already happened three times.

Why did only the blog posts lose the tag?

Our site is static. A Node script reads a registry of games and writes every crawlable page: game pages, category pages, the submission page, the challenge page, the blog index. Every one of those goes through a single function that builds the <head> — title, description, canonical, Open Graph, favicon, fonts, and the analytics snippet.

Blog posts do not go through it. They are hand-written HTML, and the generator carries an explicit instruction not to touch them, sitting right above the list of posts:

// Posts are listed by hand here so the generator never overwrites
// the hand-authored post HTML.
const BLOG_POSTS = [ ... ]

That comment is not a mistake. Regenerating a hand-written article would destroy the article. But read it as a sentence about analytics and it says something else: the head template cannot reach these files. Anything the template guarantees — the tag, and anything else you later add to it — stops at the boundary.

So the tag arrived by hand, thirteen times, and then did not arrive three times. There was no moment of carelessness that anyone could point at. The mechanism simply had no floor under it.

Which pages does a template actually protect?

We counted, because the ratio turned out to be the whole story. Of 235 pages this site ships:

How the page is producedPagesWhere the tag comes from
Built by the generator from the head template215The template. Cannot be forgotten.
Copied verbatim from a hand-maintained folder3A person, once, in the source file.
Mirrored from the hand-edited homepage1A person, once, in that file.
Hand-authored blog posts16A person, every single time.

The 215 are safe by construction. The 4 in the middle rows are safe in practice, because they are edited rarely and the tag is already sitting in them.

The 16 in the last row are the exposure, and it is not a coincidence that the failures were all there. It is the only row where the human step repeats. A rule that has to be re-obeyed on every new file will eventually not be, and the number of chances is the number of files you plan to write.

Worth naming the general shape, because almost every static site has all three: generated pages, copied pages, and hand-written pages. Whatever you believe is on every page of your site is only guaranteed on the first kind.

How do you find untracked pages on your own site?

The check is trivial. Search your built output for your measurement ID and list every file that does not contain it. Ours reported three:

3 page(s) missing the analytics tag:
    blog/the-same-url-served-two-different-images/index.html
    blog/the-star-rating-your-iframe-added/index.html
    blog/the-game-metadata-field-nobody-fills-in/index.html

Two things make the difference between that check being useful and it being theatre.

1. Audit what you ship, not what your build folder contains

Our output directory holds 347 HTML files. Only 235 of them ship. The other 112 are directories for retired pages the generator has never deleted, plus one snippet file that is not a page at all — it is a block of HTML meant to be pasted into another document.

Run the audit over the folder and 112 of your results describe files no visitor can reach. Worse, the noise runs both ways: a retired page that does carry the tag inflates your pass rate, and a snippet that legitimately has no <head> reports as a failure forever. Build the list of pages you actually ship, and audit that.

2. Break the check on purpose before you believe it

A check that has never failed is not a check — you have no evidence it can detect anything. So before trusting ours, we deleted the analytics snippet out of one post and re-ran the build:

md5 before strip : 187fbf210f520e496e97f082c41cf25b
tag occurrences  : 0          <- deliberately broken
build            : "inserted the analytics tag into
                    hand-authored /blog/the-star-rating-your-iframe-added/"
md5 after repair : 187fbf210f520e496e97f082c41cf25b   <- identical

That single test proves three separate things at once: the check notices, the repair works, and the repair is byte-for-byte non-destructive. A hand-written article is exactly the kind of file where "it probably re-wrote it correctly" is not good enough.

What we changed, and why it is not a checklist item

The tempting fix is a note in the authoring instructions: remember to paste the snippet. That is what we effectively had, and it produced thirteen successes and three silent failures.

Instead, the generator now repairs the gap itself. After it writes the blog index, it walks the list of posts and, for any post whose file does not already contain the measurement ID, inserts the same snippet every generated page uses, immediately before the closing </head>. Four properties made it safe enough to run unattended on hand-written prose:

The same pattern already existed elsewhere in this generator, for a different hand-edited file that had gone stale nine times in five weeks. That is the tell: when the second instance of a class of bug appears, the fix is a step in the build, not a better memory.

Untracked is not the same as unpublished

An awkward detail, recorded because it is the reason this cost nothing. The same three posts were also the three not yet deployed — they return 404 on the live site right now, verified against a deliberate nonsense URL that returned an identical 404 body. So no real visit went unmeasured.

That overlap is luck. The two failures are unrelated and they behave in opposite ways:

Unpublished pageUntracked page
What it returns404200
How it looksObviously brokenPerfect
How you find outImmediatelyWeeks later, in a report
What it looks like when you doA missing pageA page nobody visited

That last cell is the expensive one. An untracked page does not report as an error, it reports as indifference — and a page that appears to have no audience is a page someone eventually decides not to write again.

Two probes that lied while we were fixing it

Both of these happened during the repair above, both are on Windows, and both are reproducible in three lines. They are here because each one is the same species of problem as the bug itself: a thing that quietly does not do what it appears to do.

A text-mode file read rewrote a file we only meant to inspect

Opening a file in Python's default text mode, reading it, and writing the identical string back is not a no-op on Windows. Line endings are translated on write. A three-line file with Unix endings, read and written unchanged, gained three bytes:

printf 'alpha\nbeta\ngamma\n' > lf-control.txt

python -c "d=open('lf-control.txt',encoding='utf-8').read();
           open('lf-control.txt','w',encoding='utf-8').write(d)"

carriage returns before : 0
carriage returns after  : 3

On our 253-line article that turned into 253 changed bytes across the whole file, which is why the byte-for-byte check above initially failed. It was not the repair that changed the file; it was the tool used to inspect it. Open in binary mode, or pass newline='', when the bytes are the thing you are checking.

The probe that said the file was clean returned zero either way

Worse, the obvious way to check for the damage said there was none. Searching the file for a carriage return reported 0 — on a file that provably contained three:

printf 'alpha\r\nbeta\r\ngamma\r\n' > crlf-control.txt

od -c crlf-control.txt | grep -o '\r' | wc -l   -> 3   (they are there)
grep -c $'\r'  crlf-control.txt                 -> 0   (says clean)
grep -c $'\r'  lf-control.txt                   -> 0   (also says clean)
grep -cU $'\r' crlf-control.txt                 -> 3   (correct)

Without -U, the file is treated as text and the carriage return is stripped before the pattern is applied, so the search can never match. Note the third line: the probe returns 0 on a file that has them and 0 on a file that does not. It is not inaccurate, it is inert — there is no input that would make it say anything else.

That is the general rule worth taking away, and it is the same rule that found the original bug. Run your checker against an input you know should fail. If it passes that too, the checker is not measuring anything, and every green result you have collected from it means nothing.

How to check yours, in about ten minutes

  1. Write down your ship list — the pages a visitor can actually reach. Not your build folder, and not everything your generator has ever emitted.
  2. Grep that list for your measurement ID and print the files that do not contain it. Expect the count to be greater than zero the first time.
  3. Sort the misses by how the page was produced. If they cluster in one production path, you have found a structural gap, not three mistakes.
  4. Delete the tag from one page and re-run. If the check still passes, fix the check before you trust a single result from it.
  5. Move the guarantee into the build, and make it print a line when it repairs something.
  6. Repeat with anything else your template promises — canonical links, viewport meta, structured data, consent scripts. They all stop at exactly the same boundary.

What we could not verify

Stated plainly, because a post that only reports its wins is not worth much:

Related reading

Making something playable?

SlowDen is a place to put a browser game where people can find it and play it — no download for the player, no store page for you. Games here come from creators, partners and third-party sources, and each one is credited to whoever made it.

Slow Cook is our creative challenge round. You get a prompt, you build something around it, you submit it for consideration. Selected entries have a chance to be featured, and selected winners may receive a shoutout across SlowDen’s social accounts. That is the honest list; there is nothing guaranteed beyond it. Check the Slow Cook page for the current round and its closing date.

Submit a browser gameSee the Slow Cook challenges

Questions people ask

Why do some pages of my site have no Google Analytics tag?

Almost always because those pages did not come out of the function that writes your head element. On a static site there are usually three kinds of page: pages the generator builds from a template, pages the generator copies verbatim from somewhere else, and pages a human wrote by hand. Only the first kind is protected by the template. We counted ours: 215 of 235 shipping pages are built by the template, and the remaining 20 depend on a person remembering. Three of those 20 were missing the tag.

How can I find pages on my own site that are missing the analytics tag?

Grep the built output for your measurement ID and list the files that do not contain it, then check that list against the set of files you actually ship. Two cautions from doing it on our own site. First, run the check against your ship list, not your output directory: ours holds 347 HTML files of which only 235 ship, so an audit of the folder measures 112 files that no visitor can reach. Second, break the check on purpose before you trust it. We deleted the tag from one post, re-ran the build, and confirmed the check failed and the repair restored the file byte for byte.

Should the fix be a checklist item or a build step?

A build step. Thirteen of our sixteen posts carried the tag purely because a person remembered thirteen times in a row, which is a streak rather than a fix, and the streak broke three times without anyone noticing. We replaced it with a step in the generator that inserts the tag into any hand-authored post that lacks it, immediately before the closing head tag. It is insert-only, it never rewrites the author's prose, and a post that already has the tag is not touched at all.

Why would a generator refuse to overwrite a page in the first place?

Because some pages are genuinely written by hand and regenerating them would destroy the work. Our blog posts are hand-authored HTML and the generator is explicitly told not to touch them, which is correct. The side effect is that the one page type written by a human is the one page type the head template cannot reach. The refusal is right; what was missing was a step that adds back the few things every page must carry regardless of who wrote it.

Does a missing analytics tag show up anywhere on the page?

No, and that is the whole difficulty. The page renders correctly, validates, carries its canonical link and its structured data, and returns a normal 200. Nothing about looking at it reveals the gap. It surfaces only as an absence in a report you may not read for weeks, by which point the page looks like a page nobody visited rather than a page nobody measured.

What is the difference between a page being untracked and a page being unpublished?

An unpublished page returns 404 and you find out immediately. An untracked page returns 200 and looks perfect. In our case the same three posts happened to be both, because they were the three most recent and had not been deployed yet, which is the only reason the missing tag cost nothing. That overlap was luck, not design, and it is worth separating the two failures rather than treating one as evidence about the other.