Quality Gates — Auditing the Links
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.
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.
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.
| Check | Result |
|---|---|
| Home → each work | 12 of 12 fine |
| Each work → anywhere else | 3 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.
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.
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.
| Rank | Mechanism | Notes |
|---|---|---|
| 1 | The official "related video" field | Puts a link on the player itself. Not exposed in the API — set by hand in Studio |
| 2 | Playlists | Shows what comes next. Free, and scriptable |
| 3 | An end card | Shorts loop, so do not lean on it |
| 4 | First line of the description | Assume it is never opened |
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.
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.
| Before | After | |
|---|---|---|
| Dead-end pages | 3 | 0 |
| Short ↔ long pairs joined | 7 of 14 (one wrong) | 14 of 14 |
| Playlists | 2 (for 90 videos) | 5 |
| Most-visited page's outbound links | 0 | 3 |
| Home page entry points | By category | By 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.
| Symptom | Cause | Fix |
|---|---|---|
| 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.