← New Forms Research A-8 / EN · 日本語

Quality Gates — Auditing the Links

Nobody can reach
your other work

I shipped 16 projects and 90 videos and still nobody moved between them. So I crawled my own live URLs and counted. 3 of 12 pages were dead ends. 90 videos had 2 playlists. 7 of 14 short/long pairs had no link at all.

2026-08-26 / measured on live URLs / ~8 min read

01 — The constraint

"If I make more, more people will see it" is a maker's illusion

I assumed that more projects would mean more views. It does not. What grows is the number of entrances. What does not grow, unless you build it, is anywhere for the visitor to go next.

In my head everything is connected. This story and that map share a world. This short video has a long version. Because it is obvious to me, the missing line is invisible to me.

When your catalogue grows but circulation does not, the thing you are short of is not work. It is the space between the work.

And this is a silent failure. A missing link throws no error. The page renders perfectly. The person who left says nothing, so it never comes back to you as a complaint either.

02 — What makes it fixable

Link counts are integers, so they can be a test

Circulation sounds like a matter of taste. It is not. "How many links go from A to B" is an integer. Anything countable can be turned into a check that runs.

One rule matters more than the rest: fetch the live URL, not the file on your disk. A local check happily passes over a file you forgot to commit.

def get(url):
    req = urllib.request.Request(url, headers={"User-Agent": UA})
    return urllib.request.urlopen(req, timeout=45).read().decode("utf-8", "ignore")

top = get(BASE)
for w in WORKS:                      # home → each work
    print(w, top.count('href="' + w))

for w in WORKS:                      # each work → anywhere else
    html = get(BASE + w)
    print(w, count_outbound_links(html))

Twenty lines. Here is what it printed the first time I ran it.

CheckResult
Home → each work12 of 12 fine
Each work → anywhere else3 of 12 were dead ends

The three dead ends were the three pages search sends the most people to. A map, a pop-up book, a travel journal. Each one feels self-contained while you are building it, so it never occurs to you that someone standing there might want to leave.

03 — The method

Four things to count

① Count exits, not entrances

Everyone checks "does the home page link to everything." The direction that gets missed is the other one: does each page have somewhere to go. Count it for every page. Zero means dead end.

The fix is small — one shared link on every page — but after adding it you must check that it can actually be clicked. On pages with a full-screen UI, a new element can land underneath something else.

hit = pg.evaluate("""() => {
  const a = document.querySelector('.homeback');
  const r = a.getBoundingClientRect();
  const top = document.elementFromPoint(r.x + r.width/2, r.y + r.height/2);
  return top === a || a.contains(top) ? 'clickable' : 'covered';
}""")

elementFromPoint returns whatever is actually on top at that coordinate. "Visible" and "clickable" are different properties, and isVisible() only answers the first one. Three of my pages were covered by a start dialog; I only found out because I checked.

② Find the pairs that exist but were never joined

I had 90 videos. More than half are short-form, and most have a long-form counterpart. Of 14 such pairs, 7 had no link between them — and one short was linked to another short, which does nothing for the long video.

Playlists: 2, for 90 videos. I built three, ordered as "short → its long version → next short → its long version." The ordering is the point: sequence them by what the viewer wants next, not by release date.

RankMechanismNotes
1The official "related video" fieldPuts a link on the player itself. Not exposed in the API — set by hand in Studio
2PlaylistsShows what comes next. Free, and scriptable
3An end cardShorts loop, so do not lean on it
4First line of the descriptionAssume it is never opened

③ Identical on screen does not mean identical underneath

While wiring those pairs, a search returned two candidates with exactly the same title. I had uploaded the same video twice, years apart in attention, and left one unlisted.

On screen the two were indistinguishable. No badge, no date, nothing. Pick the unlisted one and no error appears — only the person who clicks finds out.

What separated them was the thumbnail URL. It is shaped i.ytimg.com/vi/(video-id)/, so the identifier is in the image source even when the label is identical.

const bg = card.querySelector('.thumbnail').getAttribute('style');
const id = bg.split('/vi/')[1].split('/')[0];   // the real identifier
if (id === WANT) card.click();

When two options look the same, select by identifier, not by label.
A correct-looking screen is not evidence that you picked the correct thing.

④ Never trust "saved"

Admin panels display the right value after saving. But they display only the title — so with two identical titles, the screen cannot tell you which one went in.

So the last step is to fetch the public page and count identifiers.

html = get("https://www.youtube.com/shorts/" + short_id)
print(want, html.count(want))    # correct id   → 5
print(dupe, html.count(dupe))    # wrong id     → 0

Five and zero. That is the first moment you are allowed to say it worked. Verify where the visitor stands, not where you stand.

04 — Results

What changed

BeforeAfter
Dead-end pages30
Short ↔ long pairs joined7 of 14 (one wrong)14 of 14
Playlists2 (for 90 videos)5
Most-visited page's outbound links03
Home page entry pointsBy categoryBy time: 3 / 10 / 30 minutes

The last row was not something I counted — it was something I noticed while counting. The home page told visitors what existed and never answered whether they had time for it right now. A first-time visitor can judge minutes. They cannot judge categories.

See it

Everything below is free and runs in a browser. Pick by how long you have.

Open the shelf
05 — Run this on your own things

Five steps

  1. Put your catalogue in one array. The list in your head has holes; writing it down reveals them
  2. Fetch live URLs and count exits. Not local files. Zero means dead end
  3. Enumerate the pairs — short and long, article and project, source and adaptation — and count the links between them
  4. After adding a link, prove it is clickable. Visible ≠ clickable
  5. Confirm by identifier, on the public page. Labels lie when duplicates exist

Failure modes

SymptomCauseFix
Catalogue grows, circulation does not Adding entrances, never exits Run a zero-exit check on every page
The link is there but nobody clicks It is underneath a full-screen UI elementFromPoint to confirm it is on top
You set it, it did not take You verified on the admin screen Count identifiers in the public page HTML
You picked the wrong one of two The picker only shows labels Select by the id embedded in the thumbnail URL
The audit reports zero but links exist The page builds hrefs at runtime Open it and interact before believing the number

That last row cost me a wrong conclusion. Two of my pages attach their links only when you walk up to a place or open a book, so a static crawl reported them as isolated. They were not. An audit you act on has to be correct first.

In the time it takes to make one more thing, you can repair the paths between the ninety you already made.
This week, the second one was clearly the better trade.

More notes in New Forms Research. Everything is free and browser-only: the shelf.