Skip to content

Add hub pages at /drink/, /menu/<family>/ and /shape/<pattern>/ - #126

Merged
cjimti merged 1 commit into
mainfrom
seo-hub-pages-and-static-links
Sep 19, 2026
Merged

cjimti merged 1 commit into
mainfrom
seo-hub-pages-and-static-links

Conversation

@cjimti

@cjimti cjimti commented Sep 19, 2026

Copy link
Copy Markdown
Owner

Every row on the Menu is written by app.js, so the address a crawler reads at fewbottles.com/ linked none of the 185 drink pages under it. The sitemap listed them and nothing else did. A sitemap is discovery only: it carries no anchor text, no context and no importance, so the drink pages sat as orphans off the one page with external links pointing at it, and any crawler that does not render JavaScript saw a site with no interior.

This adds the pages that make that path exist, and the checks that keep it existing.

What is new

scripts/pages.py writes 25 hub pages beside the 185 drink pages.

  • /drink/ indexes every drink A to Z, each with the bottles that go in it.
  • /menu/<family>/ is one section of the printed card. 15 of them, from Gin at 48 drinks to Absinthe at 1.
  • /shape/<pattern>/ is one of the shapes kin.py files. 9 of them, from Martini at 44 to Old-Fashioned at 8.

Both filings are already data, family in data/cocktails.json and pattern in data/kin.json, so neither is recomputed here and a third filing is not invented. leadBottle stays in app.js, because the All view is a view and not an address.

The link graph this produces

Measured on the committed HTML, not asserted:

static inbound links
Drink pages with zero 0 of 185
Fewest any drink has 3
Most 23

Those come from 185 links on /drink/, 185 from the family hubs, 185 from the shape hubs and 1,110 from the kin lists the drink pages already carried. The front page still links no drink directly, which is correct, it links /drink/ twice instead, so every drink page and every hub is two clicks from the front page in raw HTML with nothing running.

Each hub also links every other hub with the count of drinks it holds, each drink page links the section and the shape it belongs to, and index.html links /drink/ from the Info tab and from the <noscript> fallback. Those two are the only hand written links added; everything past them is generated, so the front page cannot drift from the menu.

Structured data

A drink page's BreadcrumbList is four steps now rather than two: the front page, the index, its section, the drink. Each hub carries a CollectionPage over an ItemList of what it lists, on the same #person and #website ids index.html already uses. sitemap.xml is 211 URLs, with the hubs between the front page and the drinks and each one dated by the newest drink it lists.

Checks added, because a link graph rots quietly

  • check_assets.check_present: every local reference from every served file resolves to a file that is in the repo. Nothing checked this before, and it is what catches a hub link with no page behind it.
  • check_assets.check_stamps now fails on a served page that links the stylesheet with no release stamp to land on, so a new kind of page cannot ship taking the edge's four hour old app.css.
  • check_assets.check_files resolves absolute paths. It could not before, which is why /drink/ in index.html was the first thing to break the build.
  • stage.py reads which pages take the ?v= stamp off pages.OWNED rather than a regex of its own, and SERVED carries menu/ and shape/.
  • check_style.page_files and check_assets.served_texts read pages.OWNED too, so the hubs are linted, alt-text checked and dash checked exactly like the drink pages. 212 static pages now, from 187.
  • pages.py refuses a section or shape id that is not a slug, and an orphaned hub is reported and deleted the same way an orphaned drink page is. Tested by planting stray hub pages and running make pages.
  • Every BreadcrumbList item must be an absolute URL on the origin.
  • test_checks.py goes from 316 to 322 cases.

One consequence worth agreeing to before merge

A family id and a pattern id are stable keys from here on, like a cocktail id and a shelf bit. They are addresses now. Renaming gin or old-fashioned moves a page, the old address answers with 404.html, and whatever that page had accumulated is gone. CLAUDE.md records this in a new section, Being found.

Design

The hubs have no CSS of their own. They are the classes the drink pages and 404.html already carry, so light, dark and print were settled before the first hub existed. The one rule they cost is on the drink page: a is color: inherit with no underline everywhere here, so .page__eyebrow a and .page__note a join the group that marks a link, or the section and the shape read as plain words.

What the adversarial pass caught, and I fixed

  1. A relative breadcrumb URL. My new step 3 was /menu/vermouth/ while the other three were absolute. Fixed, with a check over four pages that every crumb is an address.
  2. The up-links did not look like links, per the CSS above.
  3. Grammar on a one drink section: "1 drink print under Absinthe".
  4. Copy that defined a thing by what it is not, against the rule in CLAUDE.md. Rewritten positively.
  5. The index meta description listed four alphabetical accidents, "1910, Ace of Clubs, Adonis, Affinity". Now the sentence alone.
  6. A shape link could 404 if kin.json ever named a pattern it did not define. Linked only when the hub exists.
  7. The orphan message said "is for a drink no longer on the menu", which is wrong for a hub.

What I ran

make verify green: 322 checker cases, 24 unit cases, 212 static pages linted, 211 sitemap URLs, every code still agreeing with its build.

make stage V=v9.9.9 rehearsed the release: 215 stamps land exactly once, and the two inline script hashes are unchanged, so this needs no Content-Security-Policy edit in Cloudflare before the tag.

Headless Chrome at 390 and 1280, in dark, light and print emulation, over a family hub, the index and a drink page.

Not checked: offline behaviour live. sw.js puts the hubs in the network first branch exactly like drink pages, with offline.html as the fallback, but I did not install the worker over https and pull the plug. Separately, an empty menu still dies at max(days) in the sitemap, which is pre-existing and which the index hub would now share.

Reviewer notes

  • 185 of the 223 changed files are drink pages, all regenerated with the identical 15 added and 3 removed lines. git diff origin/main...HEAD -- drink/adonis/index.html is representative of all of them.
  • /menu/absinthe/ and /menu/fernet/ are one drink pages, which a search engine may read as thin. They are honest, and dropping them would leave those two drinks with one fewer way in.
  • A hub's lastmod is the newest drink it lists, so a sibling section's count changing does not move it. Understating is the safer direction, since a lastmod that is wrong in the other direction is one Google stops reading.

Every row on the Menu is written by app.js, so the front page linked none of the 185 drink pages under it. The sitemap listed them and nothing else did. scripts/pages.py now writes 25 hub pages beside the drink pages: /drink/ indexes every drink A to Z with its ingredient line, /menu/<family>/ is one section of the printed card and /shape/<pattern>/ is one of the shapes kin.py files. Both filings come from data/cocktails.json and data/kin.json, so neither is recomputed here and leadBottle stays in app.js.

Every hub links every other hub with the count of drinks it holds. index.html links /drink/ from the Info tab and from the noscript fallback, and each drink page links the section and the shape it belongs to and the index. A drink page's BreadcrumbList is four steps now, from the front page down. Each hub carries a CollectionPage over an ItemList on the same #person and #website ids the rest of the site uses, and sitemap.xml lists 211 URLs with each hub dated by the newest drink it lists.

stage.py reads which pages take the ?v= stamp off pages.OWNED rather than a regex of its own, and SERVED carries menu/ and shape/. check_assets.check_present fails on any local reference that does not resolve to a file, which catches a hub link with no page behind it; check_stamps fails on a served page that links the stylesheet with no stamp to land on; check_files resolves absolute paths, which it could not before. check_style.page_files and check_assets.served_texts read pages.OWNED, so the hubs are linted and dash-checked like the drink pages. pages.py refuses a section or shape id that is not a slug, and an orphaned hub is reported and deleted the same way an orphaned drink page is. test_checks.py goes from 316 to 322 cases.

app.css adds .page__eyebrow a and .page__note a to the group that marks a link, because a bare anchor here inherits its colour and carries no underline. robots.txt, llms.txt, README.md and CLAUDE.md say where the hubs are, and CLAUDE.md records that a family id and a pattern id are stable keys now, like a cocktail id and a shelf bit.
@cjimti
cjimti merged commit d656923 into main Sep 19, 2026
1 check passed
@cjimti
cjimti deleted the seo-hub-pages-and-static-links branch September 19, 2026 22:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant