House UK · AU · US

What this is

A set of four search surfaces over UK parliamentary proceedings, built for journalists who need to triage faster than parliament’s own tools allow. The main Search pulls from four official APIs at once — spoken contributions, written questions and answers, written ministerial statements, and committee debates — and shows the lot in one ranked list. Deep Dive plots a single term as a party-stacked monthly timeline with click-to-filter panels for members, debates and co-occurring phrases. Yesterday is the last sitting day in one page: what was debated and for how long, the closest votes and who broke the whip, the words said far more than usual, and what was slipped out in writing. Committees finds inquiries by name, shows what’s coming up in the committee rooms this week, and lets you drill into one inquiry to search its transcripts directly. And Transcripts searches the actual words inside select-committee oral evidence — the bit Hansard’s API doesn’t expose for search, made possible here by a daily-built static index.

Use cases that drove the shape: a defence correspondent looking for every mention of RAF Lakenheath in the last year; a health editor scanning what’s been said about a specific hospital trust; a political reporter tracking how often a junior minister has spoken on a topic; a tech reporter looking for every committee witness from Meta or Roblox; a feature writer pulling all the times Wendy Williams has been raised in evidence.

How it’s made

Vanilla JavaScript and a small Cloudflare Worker. The frontend on GitHub Pages talks to five parliament APIs via a 50-line Worker proxy — needed because most of them don’t return CORS headers. Transcripts is the exception that drove a real backend addition: a daily GitHub Actions job walks the committees API, fetches each oral evidence transcript, parses it into speaker-attributed segments, and commits a JSON index back to the repo. The browser fetches that file once and searches it entirely client-side. The rolling 90-day window is rebuilt every night; the sealed quarterly archives are built once per quarter and frozen.

Built with Claude Code over a series of conversations. Source is at github.com/peter-guillam123/House. All the data is from parliament under the Open Parliament Licence v3.0.

What it doesn’t do

The transcript archive goes back to the start of 2020 — six years of select-committee evidence, around 3,500 sessions across the quarterly archives. Worth knowing that the July 2024 general election was a hard reset on committee membership and on what was being inquired into, so a search that crosses that line is mixing two different parliaments’ preoccupations. Pre-2020 is feasible to backfill if anyone needs it.

The transcripts come out of parliament’s .docx-to-HTML conversion pipeline, which sometimes sticks single-character spaces inside words (“giraffes” ends up indexed as “gi raf fes”). A search for “RAF” will occasionally false-match a standalone “raf” sitting inside another word. That’s the source data — fixing it without corrupting legitimate text would need OCR-grade reasoning.

Multi-word terms are matched as a phrase, so “RAF Lakenheath” finds that exact phrase rather than RAF and Lakenheath separately. There’s no Boolean syntax (AND/OR/NOT). For more advanced queries on Hansard, fall back to hansard.parliament.uk directly.

Changelog

May 2026

First version live

Four sources, five filters, results sorted newest first. Built in a single planning-and-execution pass with Claude Code. The hard part turned out not to be the APIs — they’re genuinely good — but the fact that two of them don’t return CORS headers, which would have killed a pure static-site build. A 40-line Cloudflare Worker fixes that for less than the cost of a coffee per year.

Things deliberately left out of v1: alerting (email me when X gets mentioned), saved searches, CSV export. Probably the right next things to add if it gets used.

May 2026

Premium pass

Reset the surface around an editorial-archive direction: ivory paper, near-black ink, a single restrained oxblood accent. Source Serif 4 for display, Inter for body, JetBrains Mono for stamps. Sticky search on mobile, collapsible filter shell, generous result rhythm. New favicon and OpenGraph card. Functionality unchanged.

May 2026

Deep Dive on mobile

Tightened the Deep Dive page on phones. The chart was sitting in a padded box and capped at 200px deep, which flattened the party-stacked shape; reduced the inset and let it breathe to 280px. The two leaderboards now show the top five by default with a “Show all” reveal, so the actual contributions list isn’t buried half a screen down.

May 2026

Panels as filters

The leaderboards and the legend on Deep Dive turned out to be better filter surfaces than any panel of dropdowns would have been — they’re already organised by who’s spoken most, which debates are carrying the topic, and which parties are doing the talking. So they now double as filters. Click a member, a debate or a party chip in the legend and the contributions list narrows; clicks AND-combine across the three axes; an active-filter strip sits above the list with one-tap removal. Shareable in the URL.

The chart itself stays as the universe view — the filters narrow the contributions list, not the shape of the conversation. The leaderboards don’t re-rank either, so they stay usable as navigation while filters are active.

Along the way: extracted a shared lozenge style so party tags look the same wherever they appear; nudged the palette (SNP yellow was disappearing on cream paper, Lab/Co-op was bleeding into Lab); and sharpened the dek to make the spoken-contributions-only point properly. Legend chips on desktop now reveal each party’s running total on hover.

May 2026

Quoted searches highlight properly

Searching for “the Guardian” in quotes was finding the right contributions, but the highlights were going wrong in two places. Our snippet highlighter was looking for the literal quoted string in the result text — and the text doesn’t have those quote characters around the phrase, so it found nothing. Hansard’s own ?highlight= parameter on click-through couldn’t parse the quoted form either, and was marking just “the” on its own. Strip the wrapping quotes for highlight purposes; leave the quoted form intact for the API search itself, which uses it to do phrase matching.

May 2026

Form polish, search pulse

Two more polish items. The Deep Dive form now matches Search on mobile — icon + input + button on one row, with the year range on a quiet second row below. Both forms pulse a thin gradient bar at the bottom edge while a search is in flight: quieter than the colour change at submit, more useful on a Deep Dive that can stream for thirty seconds.

May 2026

One shape for every result row

Search and Deep Dive had drifted into different visual idioms for the same job. Search rendered everything in mono caps with party as an inline (LAB) text and House as a tiny mid-line label; Deep Dive used sentence case and a coloured party lozenge. Pulled both onto a single shape: mono-caps date, sentence-case member name, coloured party lozenge, neutral outlined House lozenge. Side effect of the typography change: minister-by-role attributions like ‘The Parliamentary Under-Secretary of State, Ministry of Housing, Communities and Local Government (Baroness Taylor of Stevenage)’ became readable rather than a wall of caps.

May 2026

Three lifts that aren’t in Hansard

A small strategic pass — what can the page do that Hansard’s own search can’t? Three things at once.

An Export button next to the contributions count on both Search and Deep Dive downloads the current filtered set as Markdown — date, member, party, House, debate, full passage text, and a deep link back to Hansard. Drops straight into NotebookLM, Obsidian, or a Google Docs paste. Since the click-to-filter UI already lets you curate by member, debate, party or co-term, the filter system is the research basket; no need for a separate starring affordance.

A new ‘Often mentioned alongside’ panel beside the existing leaderboards on Deep Dive lists the multi-word capitalised phrases that recur across the matching contributions. For Windrush you get Empire Windrush, Wendy Williams, Home Office, Caribbean Community surfacing — a tipsheet in a panel. The first version drowned in noise (Hon Member, Madam Deputy Speaker, Liberal Democrats, ‘Recent Developments’ as a Hansard heading template); a couple of rounds of stopword tightening on real samples cleaned it up. Click a phrase to filter to contributions that mention it, AND-combined with the other filters.

And the stats row picked up small sub-lines under each cell: the peak month’s count, who first mentioned the term, who most recently raised it.

May 2026

Searching the words inside transcripts

The big one for select-committee evidence. Hansard’s API doesn’t expose transcript text for search; the committees API exposes the documents as base64-wrapped HTML but not as searchable content. So a daily GitHub Actions job walks the last 90 days of oral evidence sessions, fetches each transcript, parses it into speaker-attributed segments, and writes evidence-index.json at the repo root. Static-site search, no backend.

The new Transcripts page loads the index on entry and runs queries entirely in the browser. Snippets carry the speaker label, the previous turn for context (so a hit in a witness’s answer comes paired with the question that prompted it), and a text-fragment deep link that opens the published transcript scrolled to the line. Click-to-expand on long prior turns.

The 90-day rolling window keeps the page genuinely current without ballooning the file. To go further back, the page carries a switcher of sealed quarterly archives. Each quarter is built once, frozen, and lived alongside the rolling index. Only one is loaded at a time — keeps memory bounded on phones.

OCR-mangled words from the .docx-converted transcripts (“giraffes” indexing as “gi raf fes”) still show up. That’s the source data — fixing it without corrupting legitimate text would need OCR-grade reasoning. Live with it; flag in this About; move on.

May 2026

Six years of archive, tabular picker, smarter acronyms

The Transcripts page now spans the start of 2020 to the present rolling 90 days — about 3,500 sessions across 25 quarterly archives plus the rolling index. The session counts tell their own political story when you look at the grid: the 13-session 2024 Q3 (election dissolution + recess), the small Q3s every year for summer recess, the COVID scramble in 2020 Q2.

The pill-row layout that worked for two or three quarters would have been unmanageable here, so the picker became a Year × Quarter table with a session count in each cell — readable as a story at a glance, and there’s room to add more years before it strains.

And acronyms: searching ‘SAS’ was matching ‘sas’ inside ‘passage’ and ‘Sasaki’; ‘DEFRA’ was hitting random letter strings. Short all-caps terms (2–8 chars) now match case-sensitively with word boundaries everywhere — Search snippets, Deep Dive snippets, within-inquiry search, and the global Transcripts search. Mixed-case terms keep the existing case-insensitive behaviour. Centralised in one helper, one rule.

May 2026

Party labels colouring in

The Deep Dive monthly chart was bucketing ministers as ‘Unknown’ — Hansard attributes them by role, so ‘The Chief Secretary to the Treasury (Darren Jones)’ has no party in the parens to extract. The leaderboard chip got to LAB in the end via a separate Members API call, but the chart never got the memo: same data, two answers, sitting next to each other.

Now the chart keeps a per-month per-member tally on the side and re-derives the party split once the Members API resolves, so the bars colour in. The Members lookup also widened from the top twelve to any contributor missing a party badge, capped at fifty per dive to keep big searches sane, and bounded to four parallel requests like the per-month fetcher.

While there: the SNP lozenge was rendering as ‘SCOTTISH NATIONAL PARTY’ in tracked caps and busting the speaker chip layout — the only party without a short-form mapping. Fixed.

October 2026

What’s coming up in the committee rooms

The Committees page only ever looked backwards. Parliament publishes the forward schedule too, with witnesses attached once they’re announced, so the page now opens with a Coming up strip: next week’s public oral evidence sessions, grouped by day, with the time, the room, the inquiry and who’s giving evidence. Six rows by default and a reveal for the rest, so it sits above the record without crowding it. Private meetings are left out, because there’s nothing to attend or quote.

The more useful change is quieter. A witness or organisation search now returns scheduled sessions as well as past ones, tagged Upcoming at the top of the list, so “when is Ofcom next in front of a committee?” has an answer. Opening an inquiry shows its scheduled sessions above the transcripts. Parliament’s own search of this data doesn’t match witness names, so the page fetches six weeks of events once and filters them itself.

Every upcoming session has an Add to calendar link. Parliament serves a calendar file per event, but titles each one “Untitled”, which is no use in a diary. So House writes its own entry with the inquiry, the committee, the room and the witness list. Sessions are usually announced about a week ahead, witnesses can be added late, and times slip, so treat it as a plan rather than a promise.

Later the same day: the first version made every phone download a file, which on an iPhone means a detour through Downloads. Now each session offers Google (a link that opens a pre-filled event, no file) or Apple and Outlook (a real file the hourly job writes per session, which a phone opens as an event with an Add button). The subscribe box has Google, Apple and Outlook buttons too. Android is Google Calendar, so the Google buttons cover it.

October 2026

Committee calendars you can subscribe to

Adding one session to a diary is fine; keeping up with a beat isn’t. So the Coming up strip now ends with a subscribe box. Pick a committee, or all of them, and either open the feed in Apple or Outlook calendars or copy its address for Google. Subscribe once and it keeps itself current.

Behind it is a small Python script and a GitHub Actions job, the same arrangement that builds the transcript index. Four times a day it reads the next eight weeks of committee events from Parliament’s API, keeps the public oral evidence sessions, and writes one calendar file per select committee plus one for everything. It commits only when something has changed. Each entry has a stable ID, so when a committee moves a session or adds a witness, the entry in your calendar updates rather than doubling. A cancelled session disappears.

Parliament publishes the same data but not in this shape: its per-event files are titled “Untitled” and there is no feed to subscribe to. Calendar apps re-read a subscribed feed somewhere between every few hours and once a day, so treat the diary entry as a reminder and the committee’s own page as the final word.

October 2026

Yesterday in Parliament

Radio 4 used to do this better than anyone: a short account of what happened in the chamber the day before, for people who weren’t there. Parliament’s own sites can tell you everything that happened, which is a different thing. So House now has a Yesterday page: one sitting day, read top to bottom in a couple of minutes, built each morning from five of Parliament’s APIs.

The bits I wanted most: the words said far more than usual that day, measured against the previous fifteen sitting days, each one a link straight into Search for that day’s mentions; the divisions sorted closest first, with anyone who voted against two-thirds of their own party named; who spoke most, by words, with the chair’s procedural lines left out; and the written statements, which is where news gets slipped out on a Thursday. Below that, every debate grouped the way the day ran (questions, statements, opposition day, legislation, Westminster Hall, the adjournment debate) with a start time, speaker count, word count and who said most. Then the committee rooms, the bills that moved, and any reports published. Commons and Lords each get their own version of the page.

None of it is fetched live. A Python script walks the APIs the morning after a sitting, twice, because Hansard keeps filling in for a few hours, and commits a 40KB file per day. The page reads that file, so it loads instantly on a phone and the archive grows by itself: step back a day at a time with the links under the date. The script seeded the archive with the previous fifteen sitting days so the spike comparison had something to compare against from the first morning.

Rough edges worth knowing. Hansard’s timestamps are patchy, so a debate shows its start time but not its length. The spike list is plain word counting, not understanding: it finds “inheritance tax” on an opposition day, and it will also find a surname if one member talks for an hour. The rebel rule is mechanical and says nothing about free votes. And the Lords version is thinner, because Hansard tags the Lords record far less richly than the Commons.

October 2026

A front page

Until now the home page was a search box and nothing else, which is fine if you arrive with a question and useless if you arrive wondering what’s going on. The search box is still on top. Underneath, while it’s empty, three panels: the last sitting, the next one, and the week ahead. Type anything and they get out of the way.

The panels think in sittings, not calendar days, because Parliament sits about 150 days a year and a literal “yesterday, today, tomorrow” would be blank most of the time. On a Wednesday the middle panel is today’s order of business, PMQs and all. On a Saturday it’s Monday’s. In recess it says so, and when each House comes back. On a phone, on a sitting day, today comes first.

The left panel is the Yesterday page in miniature: when the House sat, the counts, the six words that spiked, the closest vote. The right panel is the rest of the week: the big chamber moments and the committee sessions with their witnesses, three a day, linking through to the inquiry. All of it comes from files a job rebuilds hourly through the day, so the page is as fast as it was when it was only a search box. The order of business for a sitting day is published the evening before; urgent questions are granted mid-morning and show up within the hour.

October 2026

Exports that explain themselves

The Markdown export exists mostly to be dropped into NotebookLM or a similar reading tool, and two things were wrong with it for that. It only contained whatever was on screen, which after a fresh search is the first page of each source, with nothing to say the file was partial. And its header said “Date range: Last year”, which a model reading the file in March cannot turn into dates.

Now Export fetches the rest of the results first, up to five hundred, with a running count while it works, so the file is the whole search. And it opens with a plain paragraph saying what it is: how many contributions, matching what, between which two dates, from which sources, in which House, and how many matched in all if the file is capped. Deep Dive’s export gets the same paragraph. The per-entry format is unchanged: date, member and party, debate, source, a link to the official record, the full text.

October 2026

Showing the frame while it loads

The new pages read small files rather than calling Parliament live, so they are quick, but on a train they weren’t quick enough: the home page showed three empty cards for a second, the Coming up strip jumped in above the record once it arrived, and the Yesterday page opened with a heading that read “Loading the day” in the display face.

Now each panel draws its frame first: the label it will have, a line saying it’s loading, and a few grey bars shaped like the content that’s coming, with a slow pulse that stays still for anyone who has asked for reduced motion. The page keeps its shape and nothing jumps. Screen readers get the same story through the busy state and the status line. And when a file fails to load, which on a bad connection it will, the card says so and offers a Try again rather than staying grey for good.