DEV Community

asrocky010
asrocky010

Posted on

An idempotent HTML injector in 60 lines of Node

Every static site starts the same way. A handful of hand-written HTML files, no template engine, no build step. Then you need to add the same kind of block to six of them, and you have three options: adopt a generator (and rewrite everything), copy-paste (and drift), or write a small script that edits the files in place.

This is the script, and the four decisions in it that mattered.

The shape of the problem

I needed to insert a set of collapsed question blocks into existing pages, before the call-to-action at the bottom. The rules I set:

  • running it twice must not double-insert
  • a question must never appear on two pages
  • no page should be reformatted; only a new block is added
  • a human has to be able to review the diff

1. Idempotency needs an anchor, not a diff

The naive version checks whether the content is already there. That breaks the moment you change one word in the heading, because now the check fails and the block is added again.

What works is an explicit marker on the heading:

<h2 id="temperature-question-examples">...</h2>
Enter fullscreen mode Exit fullscreen mode

The script skips the page entirely if the marker id exists. The marker is a stable identifier, the heading text is free to change.

2. Global dedupe is a second pass, not the same pass

The first version tracked used questions in a Set as it walked the pages. That is per-run state. The second run starts with an empty Set, adds ten questions to a page that already has ten, and you end up with the same question on two different pages.

The fix is to check the filesystem, not the run:

function usedAnywhere(q) {
  const a = esc(q.question).slice(0, 60);
  const b = q.question.slice(0, 60);
  return allHtml.some(h => h.includes(a) || h.includes(b));
}
Enter fullscreen mode Exit fullscreen mode

Read every page, test the stem prefix, and treat the files on disk as the source of truth. Sixty characters is long enough to be unique and short enough to survive trailing whitespace differences.

3. Insert before the last occurrence, not the first

Pages had a consistent footer CTA:

<div class="cta-stack" ...>
Enter fullscreen mode Exit fullscreen mode

The first run inserted before the first one and the block landed in the middle of the page. The fix is one call:

const at = html.lastIndexOf('<div class="cta-stack"');
Enter fullscreen mode Exit fullscreen mode

Anchoring to the last occurrence means you do not need to know how many there are, and it stays correct when somebody adds another one at the top.

4. Verify the output, not the intent

The script prints a summary. The summary is not verification. Three checks that are worth automating, because all three have bitten me:

  • the file still contains exactly one closing html tag
  • no line starts with a diff artefact, if any step shells out to a patch tool
  • for structured data, re-parse the JSON-LD and count the entries

    html.split('').length === 2

It is a dumb assertion and it caught a truncated write twice.

Why not adopt a generator

Because the pages are already written, already indexed, and the interactive parts work. Rewriting them to get a template engine is a large change to solve a small problem. A 60-line injector is reviewable in one sitting and reversible by git.

The general lesson: when you want one change across several hand-written files, write the smallest thing that makes that change reproducible. Idempotent, anchored and verifiable beats clever.

The site this came from is at https://foodsafeprep.com/ - the injector put fifty exam questions into six pages that already existed, and none of them needed a rewrite.

Top comments (0)