Filed under work
vwwwv
Filed under

work

5 entries.

Propaganda-poster mock-up of a longread magazine spread: framed illustrations of a swimmer in a flooded landscape, a flag-bearing boatman with smokestacks behind, a worker with a tool, and a tractor, interspersed with text columns and a small map of Norway.
work

Long-read, mixed-content data flow

work

Long-read, mixed-content data flow

In 2024, TV 2 published Vintersportens siste dager, The Last Days of Winter Sports, a multi-part series on what climate change is doing to Norwegian winter sports. I built the editor and the data flow underneath it.

The platform is a mini-CMS in Sanity, with schemas shaped to this article series rather than to a generic blog. Journalists composed with custom object types: framed and full-bleed photos, paired-image scrollers, table data, bar graphs comparing ski-day projections under different climate scenarios, a searchable lookup of 34 named ski resorts. Each block had its own validation and its own rendering. The schema was the editorial grammar.

The presentation is a SvelteKit app fetching content from Sanity with GROQ queries, with images served through cdn.sanity.io. My piece was the platform side: the schemas, the queries, the data flow. The journalists logged into a separate Sanity studio to do the writing; full integration with TV 2's main publishing rig was a possibility we left for later, and it was the right call.

The journalism is by Magnus Wikan, Elisabeth Teige, and Fredrik Fjellvang. The photographs are by Daniel Sannum Lauten and Elias Engevik. The frontend is by Marius Pedersen. The design is by Christoffer Sandell.

The project shipped as a one-off and stayed that way. The next iteration of long-read journalism at TV 2 wasn't built on top of this codebase; it was a separate system, picked up and grown by colleagues for the form they needed next. That, in retrospect, is the right shape for this kind of work. Move fast, get the thing to prod, get eyeballs on it, learn what works, and let the next iteration be its own thing rather than a refactor of the last one. Refactor-as-a-default slows everything down.

The most rewarding part has been watching the ideas travel. Vidar Håland took some of the techniques and concepts from this project and built a much more versatile, more integrated system for long-read journalism at TV 2. His platform powered, among other stories, the Russian Cabins investigation, which won SKUP, Norway's investigative-journalism prize. I had no direct hand in that; it was just very cool to see.

Read it: Vintersportens siste dager.

opinion

First post via the API

I am writing this from a curl command at the bottom of a long evening. The bytes are the brand. The chat is the CMS. The post is published.

I am writing this from a curl command at the bottom of a long evening. The bytes are the brand. The chat is the CMS. The post is published.

opinion

The case for treating drafts as state, not files

I built a CMS this weekend in which the editor is a chat window and the publish action is a tool call. There is no editor. There is no draft folder. There is, instead, a row in a database with a status column, and a conversation that knows how to read and write that row.

I built a CMS this weekend in which the editor is a chat window and the publish action is a tool call. There is no editor. There is no draft folder. There is, instead, a row in a database with a status column, and a conversation that knows how to read and write that row.

The architecture follows from a simple observation: I had been using Claude as my drafting tool for six months. Every "blog post I should write" was already a conversation. The only step I never finished was the part where I copy-pasted to a markdown file, committed it, and pushed.

So I removed that step. Here's what fell out.

Drafts stop being files

When the source of truth is a database row with status = 'draft', you don't have to decide where the file lives, what its filename should be, whether the slug is final, what branch it's on, or whether your laptop or your phone has the latest copy. The row knows. The row is the same row regardless of which device you opened the conversation from.

The "publish" affordance becomes a button

In a chat surface, "publish" is a one-tap prompt suggestion that fires an authenticated request to a Worker. The Worker writes to the database and the next request to the homepage sees the post. End-to-end propagation: thirty seconds, mostly DNS.

What you give up

A code-based escape hatch, mostly. If you want to publish a post by writing a markdown file in your editor and pushing, you have to build that path separately — a CLI that hits the same Worker endpoint. I haven't yet. It's on the list.

What you don't give up

Git. The site code is still in git. Backups, history, branches — all the normal version-control affordances continue to apply to the system. The content is in a database, where revisions are first-class rows and the publish-state is a column that can be flipped without a deploy.

This is not for everyone. If you like writing in your editor, keep writing in your editor; that workflow has been good for thirty years and will continue to be. But I've been writing fewer blog posts every year, and the bottleneck was always the saving and committing. Removing it has moved the number from zero to nonzero, which is a much larger improvement than people give it credit for.

opinion

Lighthouse 100 is a personality disorder

I have shipped, by my own count, four production sites with all four Lighthouse audit scores above ninety-nine. I am not proud of this. Each one cost me at least a day I will not get back, on something the user could not perceive.

I have shipped, by my own count, four production sites with all four Lighthouse audit scores above ninety-nine. I am not proud of this. Each one cost me at least a day I will not get back, on something the user could not perceive.

There is a normal version of caring about performance. It looks like: serve the right thing, cache the right thing, don't ship a megabyte of bundled abstraction for a contact form. There is a second version that lives in a different building. It looks like: spend an afternoon eliminating the last two CSS rules so the unused-rules audit clears, then realize the audit was scoring a sibling page that no human will ever load.

I built this site to test whether I could do the second version on purpose, with full self-awareness, and call it a feature instead of a tic. The answer is yes, but only barely, and only because the budget I set is visible to readers. The colophon says fourteen kilobytes. The bytes are the brand.

I am not recommending this. I am admitting it.

The honest test for performance work is: can you describe, in one sentence, the user behavior that gets better? "First-byte under thirty milliseconds so the back button feels native" is a sentence. "Lighthouse score went from 98 to 100" is not.

abandoned

mailto://, a webmail client with no servers

I spent two weekends building an email client that ran entirely from a mailto: link and the user's native mail handler. It worked. Nobody wanted it.

I spent two weekends building an email client that ran entirely from a mailto: link and the user's native mail handler. It worked. Nobody wanted it.

The pitch: every contact form on the web should be a mailto: link with the body pre-filled. No backend, no rate-limiting, no spam filters needed because the spammer has to open Mail.app to send it. The web was already this, in 1997, and we forgot.

I built a generator. Paste any HTML form, get back a mailto: with the field values templated in. It worked beautifully on iOS, badly on Android, and not at all on machines where the user had never configured a default mail client — which turned out to be most machines.

That last fact ended the project. You cannot ship a webmail client whose dependency is "the user has, at some point in the last decade, set up email on their computer." That is a 1990s assumption.

I am leaving the repo up. It still works for me. That is enough.