The Accordion Archive

Essays, expanded on demand.

Theme:
Color theme

The Archive

Every essay lives here. Open one. Read it. Close it. No feed, no algorithm, no interruptions.

All essays

The Web Was Never Meant to Be a Feed

The original web was a library of documents, linked together. You went looking for something, found it, read it, and left. Nobody was refreshing anything. The feed changed the model: instead of you seeking, content seeks you. That shift is not neutral. It changes what gets made.

A feed rewards frequency over quality. If you post once a month, an algorithm will bury you. If you post five times a day, you win attention you may not deserve. The medium quietly selects for people who can produce constantly, not people who have something to say.

But a document asks nothing of you. It sits there. You can open it on a plane with no signal, hand it to a friend, print it, archive it. It does not know how many people read it and does not care. That indifference is a feature.

When you write a page instead of a post, you write for a reader, not for a ranking. You take your time. You structure the argument. You put the takeaway at the end because you trust the reader to get there.

Takeaway: the feed is a business model, not a medium.

Why Your Nsite Will Outlive Your Framework

The average frontend framework has a useful life of about four years. The average HTML file, if you leave it alone, lasts as long as the server hosting it. This is not a knock on frameworks. It is a statement about what you are actually building.

Every framework you add to a project is a promise. It promises that its authors will keep maintaining it, that its breaking changes will be documented, that its ecosystem will not collapse. These are fine promises. They are also other people's promises, not yours.

A nsite makes fewer promises. It says: here is a file. It works today. It will work tomorrow. If the server changes, copy the file. If the browser changes, the file still renders. If you want to add a search box, you add it. If you want to remove the site, you delete the file. Nothing else is involved.

The cost of a nsite is that it cannot do some things. It cannot authenticate users, run background jobs, or query a database on each request. That list of things is shorter than people think. Many sites that "need" a framework are really just sites with a bit of JavaScript for a form.

Takeaway: write down what your stack promises, and ask who is on the hook if the promise breaks.

Notes on Writing HTML by Hand in 2026

Nobody teaches HTML by hand anymore. It is taught as a first step before the "real" work of frameworks and bundlers. That is backward. Hand-written HTML is the most portable skill in frontend development, because it is the only one that browsers have agreed to keep running.

When you write HTML by hand, you notice things. You notice that a <div> has no meaning, that a <button> does, that a heading is a structural choice and not a font size. You notice when the content order is wrong, because you have to type it in order. These small acts of noticing are the entire discipline.

The browser is more capable than most developers remember. It will validate your forms, show the right keyboard on mobile, announce your errors, remember your scroll position, and manage focus. Every one of these things is easy to break with JavaScript and hard to fix once broken.

Hand-written HTML is also the only code you can read in twenty years. A <p> written in 1996 still renders today. The same cannot be said for any framework.

Takeaway: the browser is not the problem. The browser is the platform you already have.

The Case for Fewer Dependencies

Every dependency is a bet on someone else's priorities. You bet that they will not break their API, that their bugs will not become your bugs, that their release schedule will not become your release schedule. Sometimes the bet pays off. Sometimes you spend a weekend upgrading because a transitive dependency changed its peer requirements.

The simplest way to have fewer bugs is to have fewer moving parts. A node_modules with 800 packages has 800 chances for something to be wrong. A file with no imports has zero. This is not an argument against all dependencies. It is an argument for counting them.

When you do need a dependency, pick it the way you would pick a contractor. Look at its history. Look at how it handles bug reports. Look at how often it breaks. A boring library that has been stable for five years is usually a better bet than a clever one released last month.

The best dependency is the one you can remove. If your code is structured so that swapping a library is a one-day job, you have already won most of the argument.

Takeaway: the strongest dependency is the one you don't have.

What Relays Taught Me About Resilience

A relay is a server that stores events and hands them out. It does not have to know anything about the protocol beyond a few message shapes. It can be run on a Raspberry Pi. It can go down for a week and come back. Clients that talk to five relays keep working when three of them disappear.

The lesson is not specific to relays. Any system that depends on one server is a system with one point of failure. This sounds obvious, but it is remarkable how much software is built as if the single server will always be there.

Resilience is not about avoiding failure. It is about making failure small. A single relay going down is a small failure. A single database going down is a large one. The difference is whether the system was designed with more than one path from A to B.

The other lesson is that simple systems fail predictably. A relay that speaks four message types fails in four ways. A relay that speaks forty fails in ways nobody has thought of yet. Simplicity is a resilience strategy, not a style choice.

Takeaway: assume every part will fail, then design so that the failure is something you can name.

An Argument for Boring Infrastructure

Boring infrastructure is the kind that has been running for years without anyone noticing. It does not have a release announcement. It does not have a Discord server. It has a man page and a config file and it does exactly what it says.

Exciting infrastructure is the kind that gets talked about. It has a new version every month. It has opinions about your workflow. It has a migration story. The interesting thing is that the exciting parts are usually not where the work gets done. The work gets done on the boring parts, and the exciting parts are what you talk about at conferences.

This is not a rule against new tools. It is a rule about when to adopt them. Adopt a tool when it solves a problem you have today, not when it solves a problem you might have in two years. By the time the problem arrives, the tool will be better, or you will have found a simpler one.

The best infrastructure decisions are the ones you never have to think about again. Those are rarely the interesting ones at the time.

Takeaway: boring infrastructure is a sign that you are paying attention to something else.

MemeClub: A Club, Not a Feed

Screenshot of MemeClub's board view: a single club with a name, a membership panel, and a chronological list of memes, each with a title, an image description, license and AI labels, and a comment button.
The board is a single column, newest first, with no ranking and no recommendations.

If the original web was a library and the modern web is a feed, MemeClub is a bulletin board in the back of a clubhouse. It hosts one group on one Nostr relay. You join, you browse memes in the order they were posted, you publish your own with honest labels, you comment, you leave. That is the entire product. There is no algorithm, no discovery tab, no trending rail, no recommendation engine, no analytics, and no central server that anyone can shut down or mine for engagement.

That smallness is the point. Meme sharing has become synonymous with platforms that reward volume over care: forty variants of the same image in an afternoon, half of them stolen, most of them missing alt text, several of them re-uploaded to a CDN you do not control. A club is different because its size forces a different kind of attention. When forty people are in the room, you notice when someone posts something thoughtless. The medium itself applies a little friction, and friction is what a feed was engineered to remove.

Under the hood, MemeClub is a single-file client-side application built on the NIP-29 relay-based groups spec. A group is identified by a short string and hosted on one relay, which enforces membership, roles, and moderation. Every event the app produces — a meme (kind 20), a comment (kind 1111), a join request (kind 9021), a moderation action (kind 9005 or 9001) — carries an h tag naming the group, and the client refuses to display anything whose h tag does not match. This is not a social network with a club feature bolted on. It is one club, wired directly to one relay, with no middleware in between.

Publishing a meme is deliberately harder than tapping a button. The composer requires a title, an image description, a license label, an AI-generated label, and an explicit confirmation that you have the right to share the image and that anyone identifiable in it has agreed. Content warnings are a first-class field, not an afterthought. Media is uploaded only to a Blossom server you choose, and the app refuses to display any image whose SHA-256 hash does not match the x tag on the event — which means a re-uploaded or tampered file simply never renders. There is no central uploader, and there is no way to bypass the checks: every gate is enforced by the same code path, and the tests exercise each one.

Membership has its own small state machine. You are not "logged in" to a platform; you are in or out of a club, and the app derives that from the relay's own published events rather than from a cookie. The kind 39002 member list is treated as optional and possibly incomplete, because relays often hide it. What counts is whether a moderator ever issued a kind 9000 (put-user) or kind 9001 (remove-user) that names you, and the app compares timestamps to decide which one wins. When in doubt, it shows "unknown" and keeps the moderation UI locked, rather than inventing a permission it cannot prove.

Signing is done through a browser extension via NIP-07. The app never sees, requests, or stores a private key. Every event is validated after signing: the returned event must have the same pubkey the extension advertised, the same kind, the same tags, and the same content, or it is rejected before it reaches the relay. If the relay asks for NIP-42 authentication mid-publish, the app signs one ephemeral kind 22242 event, retries once, and if the relay still refuses, it surfaces the exact message instead of retrying forever. No silent retries, no hidden background publishes, no "we'll try again later."

The type-theoretic design is not decoration. State is modelled as discriminated unions — MembershipState, ComposerState, ModerationActionState — and every reducer ends in a never check, so adding a new state without handling it is a compile error. Impossible states (uploading and published at the same time, pending and rejected at the same time) cannot be represented. Events arrive from the wire as unknown and pass through a hand-written parser that verifies the signature before the event is admitted to the store. The store itself is a join-semilattice: merging two event sets is idempotent, commutative, and associative, and addressable events are resolved by newest created_at with ties broken by lowest id. There is no array of events to accidentally duplicate; there is a Map keyed by event id.

Accessibility was designed in, not patched on. Semantic landmarks, one <h1> per view, a skip link, visible focus rings, native <dialog> elements for composer and moderation, keyboard-operable everything, 44-pixel touch targets, no colour-only meaning, no autoplaying media. Route changes update the document title, move focus to the new heading, announce politely, and reset scroll. Pagination is a real "Load more" button, not infinite scroll, because infinite scroll breaks keyboard navigation and hands the medium another way to keep you scrolling. The single-file build inlines everything, ships no framework, and runs in any modern browser with JavaScript disabled showing an honest fallback.

Harm minimisation shapes what the app refuses to do. There is no central indexer, no global moderation authority, no mass reporting, and no pay-to-rank. Mutes are local: they hide posts on your device and tell no one. Diagnostics are local: a ring buffer of the last 200 events that never leaves the browser unless you export it. Local storage is versioned, validated with a schema, and reset to defaults if it is corrupted or comes from an unknown version. Drafts are saved per group, but nothing about your key, your signer, or your relay credentials is ever written to disk. The one moderation action available to you is the one your role on the relay actually grants — and if the relay has not published a role list, the app shows no moderation controls at all, rather than guessing.

None of this is exciting. There is no growth loop, no viral mechanic, no notification stream pulling you back. But a club does not need those things. It needs a door, a board, and a rule that says: put a description on your image, label what you are sharing, and do not pretend you made something you did not. MemeClub is that rule, encoded as a client, pointed at one relay, run in your browser. The feed is a business model. The club is a place you can leave.

Takeaway: a meme club is not a smaller feed. It is a different medium — one where posting costs something, and the cost is the point.