Live: nordstream.rix1.dev
Every bulletin NRK has published in its Gassrørledningen Nord Stream feed — 263 of them, from August 2016 to today — on one scrollable page, with a timeline in the margin that shows the shape of the story.
Hybrid warfare is designed to be hard to follow. A pipeline is finished, then halted; gas is cut off; something explodes on the seabed; leaks reach the press; suspects are named, then contradicted. Each of these arrives as a small news item, weeks or years apart, and the thread between them is easy to lose. That is not an accident — it is the point.
This is an attempt at the opposite: the record, in chronological order, arranged so the pattern is visible.
The bulletins are NRK's, unchanged. What's added is structure: each bulletin is sorted into a thread (construction, politics, gas supply, the sabotage, the investigation, the courts), tagged with the countries central to it, and weighted by how much it moves the story. That sorting is done by a language model (Jev 1.13 from TypeSafe), not by a person, and it can be wrong. The red markers are picked by hand.
The site is in Norwegian, like the source material. There's a "Translate to English" link that runs it through Google Translate.
- Timeline rail — one tick per article, stacked in the left margin. Hover to magnify (dock-style) and read the headline; click to jump. Year labels sit where the year changes, so the density of September 2022 is visible at a glance. A minimap-style window tracks what's on screen.
- Red markers — a handful of key moments (pipeline completed, halted, gas cut off, the explosions, the boat lead, the arrest warrant, the admission).
- Threads and actors — every tick and every bulletin is coloured by the
thread the model put it in, with chips at the top to filter by thread and
by country. The filter lives in the URL hash, so a view can be shared:
#t=investigation&a=germany. - Fjern støyen ("clear the noise") — one toggle that reduces the page to the turning points: the red markers plus the bulletins the model rates as defining moments, about 30 in all. The rail keeps every tick, dimmed, so the gaps in time stay visible.
- About page — motivation, a time-proportional axis with every article as a tick, and a bar chart of articles per year.
deno task scrape → articles.json (NRK's serum API → structured JSON)
deno task classify → annotations.json (Jev, TypeSafe's System One API → typed labels)
deno task build → dist/ (prerendered from templates/; no network)
deno task update runs all three. There are no dependencies and no build
tooling — the output is static HTML with two small ES modules for the
interactive bits. The scripts are plain ESM and run under Node too
(node build.mjs).
scrape.mjsfetches the compilation's item list from/serum/api/content/json/<id>and each bulletin as an HTML fragment from/serum/api/render/<compilation>.<article>, then extracts headline, body, timestamp and URL.articles.jsonis committed, sogit diffafter an update shows exactly which articles are new.classify.mjssends each article, on its own, to Jev with a fixed set of questions fromquestions.js: which thread it belongs to (a choice), which countries are central (one yes/no per country), how much it moves the story (a 0–3 score), whether it repeats an earlier headline, and whether a historian would call it a defining moment. The typed, calibrated answers go toannotations.json, keyed by article id with a hash of the request, so re-running only sends new articles (or everything, if a question changed). A full run is 263 requests, about a minute, and costs about four cents.annotations.jsonis committed likearticles.json, so the build needs no key and no network, and a diff shows exactly which labels changed.build.mjsapplies the thresholds fromquestions.jsto the annotations, renders the posts and stats into the templates, writes the site todist/(not checked in), and embeds a compact article list (id,ts,headline,event, thread, actors, weight, key) that the client scripts read — the browser never parses or fetches the full data. Withoutannotations.jsonthe site still builds, with neutral ticks.data.jsholds the list of key events (by NRK article id) and is shared between the build and the browser;questions.jsholds the taxonomy, the questions and the thresholds and is shared the same way.
To add a red marker, add an { id, label } entry to EVENTS in data.js
and rebuild. To correct a label the model got wrong without re-running the
API, add the article to OVERRIDES in questions.js, e.g.
'1.16185000': { thread: 'legal' } or { key: false }, and rebuild.
deno task candidates prints the model's top key-moment candidates that
aren't markers yet, for deciding what to promote or override.
deno task build # → dist/
deno task dev # serves dist/ at http://127.0.0.1:3000Any static server works; the pages use ES modules, so they won't load over
file://.
Only deno task classify needs anything more: a TypeSafe API key from
console.typesafe.ai/keys, in the
environment as TYPESAFE_API_KEY or in a .env file (gitignored). The
task runs with network access to api.typesafe.ai only. --dry-run
shows what would be sent and the estimated cost without calling the API.
Cloudflare Pages, connected to this repo. Every push to main deploys:
- Build command:
node build.mjs - Output directory:
dist
Scraping is deliberately not part of the build, so the live site always
matches the committed articles.json. A full refresh is:
deno task update # scrape + classify + build
git add -A && git commit -m "Update articles" && git pushThe articles are © NRK and are reproduced here for personal, non-commercial reference. Every bulletin links back to the original on nrk.no. Source feed: Gassrørledningen Nord Stream. For the labels, each bulletin's text is also sent, one at a time, to TypeSafe's API for classification; TypeSafe states it does not train on customer requests.
The code is MIT licensed.