Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ The reference leaf the hub now ships carries one step this repo's deploy does no
## Open decisions

- `/robots.txt/` and `/osd.xml/` currently sit in `slugs.map` pointing at `/`. The first would be better pointing at the real `/robots.txt`.
- **103 carried media files are still linked from no page, and the cause of each is unknown.** The count opened at 120; the five empty gallery shortcodes accounted for 17 and are restored. What remains is a mix of further conversion losses and uploads the old platform never published, and this repo cannot tell those apart: the capture can, by comparing each post's rendered galleries against its converted markdown. The comparison that found the five is worth generalizing, and its blind spot is worth knowing — a mirror page parsed as having *fewer* figures than the markdown means the parser missed that page's markup, never that images were lost, so only a surplus in the mirror is a finding. `ORPHANED_MEDIA` in [`checks/check-url-parity.py`](./checks/check-url-parity.py) holds the count and fails if it moves either way.
- Content is capped at a fixed 720px on every screen, because PaperMod's width is four CSS variables with no responsive term and no Hugo parameter. The prose measure is right and should stay; images and galleries inheriting the same cap is the part that costs something on a wide display. The knobs, the override location, and the `--gap` trap are documented under "Customization points" in [`themes/README.md`](./themes/README.md).

## Deliberate deviations from the fleet baseline
Expand Down
21 changes: 21 additions & 0 deletions checks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,24 @@ Media is the one surface checked in **both** directions, and it has to be, becau
Its constant is an exact count rather than a bound. A ceiling would let a drop leave slack behind for a later regression to hide in, so whatever lowers the count lowers the constant in the same change, and the check names the new number when it drops.

A count is all the check can observe, and two causes reach each direction: it rises when a page stops linking media **or** when unlinked media is added, and it falls when media is linked from a page **or** when orphaned files are deleted. The messages name both, because naming one would send a reader looking for a page that never changed.

**Both directions read absolute references as well as relative ones.** Hugo writes an absolute URL wherever a template resolves one against the base, which the entry-cover image on every list page does. Reading only rooted paths made those files look linked from nowhere while they were being displayed, and left a broken one unchecked in the other direction. The origin is read from the home page's canonical link rather than assumed, since staging and production build with different base URLs and a hardcoded host would check one environment's output against another's. No canonical link is a hard failure, because a guessed origin inflates the orphan count by exactly the pages that use one.

## What the orphans are

The count is not a backlog. It opened at 120 and was adjudicated against the captured live site under `blog-capture/mirror/`, which holds a crawl of the old platform including all 328 URLs the contract requires:

| | |
| --- | --- |
| 17 | conversion losses, restored — five `gallery` shortcodes emitted empty |
| 5 | never orphans, referenced only by an absolute URL the check could not read |
| 97 | uploaded to the old platform's media library and never placed on a published page |
| 1 | that platform's site icon, superseded by the favicon set at the static root |

**No image the old site served from its own uploads went unimported**, and no conversion loss remains. Anything that raises the count from here is therefore new, which is what makes the exact constant worth keeping.

Three traps in that adjudication, each of which produced a wrong answer first and each cheap to re-trip:

- **A regex cannot read nested elements.** The galleries are `wp-block-gallery` figures containing `wp-block-image` figures, and matching them by pattern reported a fictional 194-figure loss. An HTML parser gives the real number.
- **Only a surplus in the mirror is a finding.** A mirror page parsed as having *fewer* figures than the markdown means the parser missed that page's markup, never that images were lost.
- **The old platform generated an attachment page per upload, and a foreign host may also serve `/wp-content/uploads/`.** Counting attachment pages as places an image was displayed makes every unused upload look published, and matching an uploads path without checking its host attributes another site's file to this one. Both were hit here.
39 changes: 32 additions & 7 deletions checks/check-url-parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@
# resolving proves an inbound link still lands, and a reference resolving proves it names a real
# file. Neither asks whether anything points at a given file, so an image the conversion dropped
# from a page stays reachable by URL, invisible on the site, and green in both directions.
# Every one traces to the WordPress conversion rather than to anything this repo does. It opened at
# 120. Five empty gallery shortcodes across three posts accounted for 17 of them, restored from the
# captured live site, and the remaining 103 are unadjudicated: some are further conversion losses
# and some are uploads the old platform never published, and telling those apart needs the capture
# rather than this repo. The count is exact rather than a bound, so whatever lowers it lowers this
# in the same change and slack can never accumulate for a later regression to hide in.
ORPHANED_MEDIA = 103
# It opened at 120, of which 17 were conversion losses restored from the captured live site and 5
# were never orphans at all, being referenced only by an absolute URL this check could not read.
# The 98 that remain are adjudicated rather than unknown: 97 were uploaded to the old platform's
# media library and never placed on any published page, and one is that platform's site icon,
# superseded by the favicon set at the static root. Nothing here is a conversion loss, and no image
# the old site served from its own uploads went unimported. checks/README.md carries the method.
# The count is exact rather than a bound, so whatever lowers it lowers this in the same change and
# slack can never accumulate for a later regression to hide in.
ORPHANED_MEDIA = 98


def load(name):
Expand Down Expand Up @@ -83,6 +85,24 @@ def check_media(public):
return missing


def site_origin(public):
"""The site's own scheme and host, read from the artifact rather than assumed.

Staging and production build with different base URLs, so a hardcoded host would check
one environment's output against another's and silently match nothing.
"""
home = public / "index.html"
if not home.is_file():
sys.exit(f"FAIL: {home} is missing - run hugo first")
text = home.read_text(encoding="utf-8", errors="ignore")
found = re.search(r'rel=["\']?canonical["\']?\s+href=["\']?(https?://[^/"\'>\s]+)', text)
if not found:
# Without the origin, every absolute reference reads as external and the orphan count
# inflates by exactly the pages that use one. Guessing would be worse than stopping.
sys.exit("FAIL: no canonical link on the home page - cannot determine the site's own origin")
return found.group(1)


def collect_refs(public):
"""Every local asset reference in the built pages.

Expand All @@ -93,11 +113,16 @@ def collect_refs(public):
# Matching only the quoted form checks a fraction of the references and calls it a pass.
quoted = re.compile(r'(?:src|href|srcset)="(/(?:media|external)/[^"]+)"')
bare = re.compile(r"(?:src|href|srcset)=(/(?:media|external)/[^\s\"'>]+)")
# Hugo writes an absolute URL wherever a template resolves one against the base, which the
# entry-cover images on every list page do. Read as external, those files look linked from
# nowhere while being displayed, and a broken one is never checked at all.
absolute = re.compile(re.escape(site_origin(public)) + r'(/(?:media|external)/[^\s"\'>]+)')
refs = set()
for page in public.rglob("*.html"):
text = page.read_text(encoding="utf-8", errors="ignore")
refs.update(quoted.findall(text))
refs.update(bare.findall(text))
refs.update(absolute.findall(text))
return refs


Expand Down