Quartet / Quintet

2026-09-05  ·  html · seo · webdev · programming

The Page You Wrote by Hand Is the One That Stops Improving

I wrote a static site generator and added canonical and hreflang to 28 article pages. Fourteen in Japanese, fourteen in English. Until then, search engines had no way to know they were translations of each other rather than 28 unrelated pages.

The build passed on the first run. All 28 pages had it.

Two pages did not.

The pages that were missing it were the most linked pages on the site

The landing page, and its English version.

site/index.html          <- product page (Japanese)
site/index.en.html       <- product page (English)
site/articles/*.html     <- 14 articles
site/en/articles/*.html  <- 14 articles

The articles go through a shared page_shell() function. The two landing pages are hand-written HTML. They predate the generator, and their layout is a one-off, so there was never a reason to put them on the template.

So when I added the annotations to page_shell(), nothing happened to those two files.

And it was not only the new annotations.

28 articles 2 landing pages
rel="canonical" yes no
hreflang yes no
og:title / og:description yes no
og:url / og:type yes no
og:image yes yes
twitter:card yes yes

og:image and twitter:card are there, because I put them in by hand the day I wrote the page. Everything added to the template since then is missing.

The practical effect: a shared link rendered an image with a guessed title. With no og:title, a crawler falls back to <title> or whatever heading it finds convincing.

And these two pages are where the READMEs of three repositories and the footers of 24 articles send people. The most linked pages on the site had the least markup on them.

The exception you wrote by hand does not receive later improvements

Stated generally:

Improvements go into the template. Things that do not go through the template do not get them.

On the day you write it, the hand-written page is usually the better one. You spent time on it, and you built it for that page specifically. That part is not the mistake.

The mistake is what happens afterward.

Feb   write the landing page by hand   ... it is the best page on the site
May   add OGP to the template          ... only the articles improve
Jul   add structured data              ... only the articles improve
Sep   add hreflang                     ... only the articles improve

The hand-written page stays at the quality it had the day you wrote it. Everything around it keeps rising, so in relative terms it falls.

The same shape shows up all over a codebase:

Every one of those was a reasonable call when it was made. Every one of them stops receiving what lands afterward.

Checking the output beats normalizing the input

There are two ways out.

A. Remove the exception and put everything on the template.

The right direction, but not always available. My landing page has a one-off layout, and forcing it through the article template breaks it. Generalizing the template enough to hold it would make the generation of the other 28 pages more complicated. Adding complexity to 28 pages for the sake of 2 is not a trade I want.

B. Check the output, whatever produced it.

This is what I did. Look at what came out, not at how it was made.

hreflang has a reciprocity requirement. If A says "the English version is B", then B has to say "the Japanese version is A". If it does not, the annotation is ignored entirely. A one-sided declaration does nothing.

So read every HTML file that was produced and check exactly that.

import re, pathlib

BASE = "https://example.github.io"
site = pathlib.Path("site")
ann = {}

for f in site.rglob("*.html"):
    text = f.read_text(encoding="utf-8")
    url = BASE + "/" + str(f.relative_to(site))
    if f.name == "index.html":
        url = url.replace("/index.html", "/")
    # lang -> href
    alts = {m.group(1): m.group(2) for m in re.finditer(
        r'<link rel="alternate" hreflang="([^"]+)" href="([^"]+)"', text)}
    if alts:
        ann[url] = alts

bad = []
for url, alts in ann.items():
    for lang, target in alts.items():
        if lang == "x-default":
            continue
        if not target.startswith(BASE):
            continue                      # external hosts will not annotate back
        if target not in ann:
            bad.append(f"{url} -> {target} has no hreflang of its own")
        elif url not in ann[target].values():
            bad.append(f"{url} -> {target} does not point back")

print(f"pages with hreflang: {len(ann)}  inconsistent: {len(bad)}")
for b in bad:
    print(" ", b)

This check does not know whether a page came from the template. It does not need to. Hand-written or generated, if the file landed in site/, it is held to the same standard.

Managing the input means managing every path into it, and you will miss one when a new path appears. Checking the output is one place, and it holds no matter how many paths there are.

Some annotations are worth keeping even when they cannot work

Running the check gave this:

pages with hreflang: 32
pointing at external hosts: 20   <- reciprocity cannot hold
inconsistent within the site: 0

The articles are also published on other platforms, and those copies are the canonical ones. That means hreflang points at an external URL too — and those platforms will never point back. Reciprocity does not hold, so those 20 annotations are ignored.

I left them pointing outward anyway, because there are only two options:

reciprocity agrees with canonical
point at the external URL no (annotation ignored) yes
point at my own non-canonical copy yes no

A page whose hreflang and canonical disagree gets both signals distrusted. Being ignored is the cheaper failure.

Sometimes you pick the option you know does not work, because the alternative does damage. What matters is picking it knowing that. An annotation that is inert because you chose it and one that is inert because you never checked look identical today, and differ entirely the day you can fix it.

Takeaways


Related


I publish the configuration for splitting Claude Code into separate personas — Architect, Coder, Reviewer, Conflict Resolver — under MIT. Copy it, run ./setup.sh, and it works. It does not depend on your tech stack.

https://github.com/quintetkit/quartet

I built one real tool using nothing but this workflow. Every Issue, PR, review and merge is still there. The parts that went wrong were not deleted.

https://github.com/quintetkit/mdlinkcheck

The version that adds a UI Designer persona, review criteria, a per-Issue parallel execution script and a 10-chapter guide is on the product page.

The full kit — five personas, the scripts and the complete guide in English and Japanese — is on BOOTH, a Japanese store with an English interface that takes international cards.

https://quartet-dev.booth.pm/items/8807156

The workflow itself is available

Quartet, the four-persona version, is published free under MIT. Quintet adds a UI Designer persona, review criteria, a per-Issue parallel execution script, and a 10-chapter guide.

See the free version Product page