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.
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.
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.