Theatre Register

The editorial spine

864 ways into 939 shows.

flows.md says the front door cannot be a search box, because a search box serves people who already know what they want and the highest-value browsing arrival does not. The spine is the answer to that. It lives in web/collections_spine.py, with two extension modules — web/collections_people.py and web/collections_more.py — loaded lazily from it.

Two kinds, one shape

Handmade collections are written by a person. EXCLAMATION! exists because somebody noticed that forty-four shows have an exclamation mark in the title and almost all of them are pre-1940, which says something about how theatre named itself and then stopped. No generator finds that.

Generated collections come off an axis — every composer with four or more shows, every performer with six, every decade, every house. There are several hundred, and they are why you can follow Cole Porter to Ethel Merman to the Alvin Theatre without touching the search box.

Both produce the same record, so nothing downstream can tell them apart:

slug   title   standfirst   kind   sql   params   group

kind decides how a row renders and where it links — show, production, person or venue. params is always bound, never interpolated.

The groups

GroupnGroupn
On stage361Absences12
Houses105Buildings12
Songs44Partnerships12
Composer37Titles11
Directed by36People10
Choreographed by36Produced by9
Themes29Recordings8
Lyricist29Week7
Crossings22Runs7
Book16Company7
Shape13Careers6
Rights13Form4
Eras13Sources4
Places1

483 collections return productions, 352 return shows, 16 return people and 13 return venues.

Absences is the group to look at if you want to know what this catalogue thinks it is for. brief.md's stated purpose is what got recorded and what did not, and that is a question about productions. A collection of things that are missing is only believable over a complete corpus, which is the argument decisions.md §12 makes for the Broadway-and-West-End gate.

Three rules, and they have all cut something

A collection must be true. "Set in London" was cut: forty shows mention London and exactly two are set there — the rest transferred. "Berlin" was cut because half the mentions are Irving Berlin.

A title must not assert what the data cannot support. "Friday flops" became "Friday finales" once it turned out that a one-performance run is as often a gala as a disaster. The catalogue records the run, not the intention.

The standfirst is writing. "Shows where composer = X" is a filter, not a sentence. Say something, and let the number render from the data.

The weekly rota

Seven collections carry a day, and the front page leads with whichever one it is. Monday musicals, Tuesday troublemakers, Wednesday elsewhere, Thursday throwbacks, Friday finales, Saturday spectaculars, Sunday second acts. Defined in web/collections_spine.py as DAYS and DAY_LABEL.

Why counting them is a deployment concern

web/collections_spine.py:all_collections() counts every collection the first time anything asks for one. Cold, that is 864 COUNT(*) queries and about 69 seconds on an M-series laptop; on a shared vCPU it is minutes, and it lands on whichever request arrives first after a restart.

So it runs at import in deploy/wsgi.py, before gunicorn forks. With --preload, the master imports once and workers fork from it, so the built list is shared copy-on-write and two workers cost one worker's worth.

The container ships warm caches, all three stamped with the database's size rather than its mtime: a deploy does not preserve mtime, so an mtime-stamped cache is invalid the moment it lands. If a stamp is stale — someone swapped the database under a running container — the site pays for the rebuild at boot, loudly, rather than silently on a reader's first page.

web/collection-counts.json and web/collection-counts-more.json hold the row count per collection. web/collection-spine.json holds the built definitions — slug, title, standfirst, kind, SQL, parameters, group and count — and it is the one that matters on a serverless host. The counts cache saved the answers and not the questions, and the questions were the expensive part: assembling the 864 definitions runs ten aggregate queries over the whole catalogue, which measured 5.2 seconds cold. That was paid on every page rather than only the collection pages, because collection_slugs() is a context processor.

Rebuild them by deleting them and asking for the spine once:

rm web/collection-spine.json web/collection-counts*.json .venv/bin/python3 -c "import sys; sys.path.insert(0, 'web'); \ import collections_spine as s; s.all_collections()"

tools/check-deploy.py refuses a deploy whose caches are stamped for a different database from the one shipping beside them.