Minor improvements in May
A list of the smaller improvements from May. Most of the reading ones went into Feedsweep, the package that tidies up feed content before it reaches the reader, with the rest in Feedstand itself.
Reading
- Emoji that publishers ship as tiny images are swapped for the actual character.
- When an item repeats its own title as the first heading in the body, that duplicate heading is removed so the title only shows once.
- When an item's body has its own
<h1>, it collides with the reader's page title, so every heading is shifted down one level (<h1>becomes<h2>, and so on) to fix the hierarchy. - Redundant bold wrapping a whole heading is dropped.
- Wide tables now sit in their own scrollable strip with a soft fade on whichever edge still has more to scroll, instead of spilling past the reading pane and getting clipped.
- Item titles render as plain, fully decoded text with no stray HTML left over.
- Clutter is stripped out: share buttons, unrelated post widgets.
Reliability
- The fetcher now identifies itself with its subscriber count and feed id, so publishers can see who is fetching and how many readers depend on the feed.
- A feed that keeps returning 404 is given more attempts before being marked discontinued, so a brief outage no longer drops it.
Auth improvements and passkey support
The authentication screens got a long-pending visual refresh. During development they were always second priority and never truly finalized, so I gave them a proper pass this time around. All the auth flows now share one consistent look.
Email verification after signup is wired up, and password reset links go out by email. Plain text for now.

Another recent improvement is support for passkey login. You can now register passkeys in the settings and use them the next time you sign in. To learn more about passkeys, visit the official website.
Feed discovery optimizations

When you paste a site URL into Feedstand to add a feed, there used to be a short wait while it visited the site, scanned the page, and figured out which feed was the right one. That wait is gone for sites that were already seen. The feed shows up right away.
Given that Feedstand has a large collection of feeds already, there's a good chance the site you're pasting is one of them. When it is, the feeds come back straight from what's stored, no visit to the site needed.
Extracting Feedsweep and frontend optimizations
This month I extracted yet another chunk of the feed processing into its own package.
Most feeds need work to display properly in a reader — fixing lazy-loaded images, resolving relative URLs, normalizing broken markup, embedding media, and similar de-quirking. The code for all of that (and more) has been growing inside Feedstand for a while. Recently, this special treatment of feeds got quite big, so I cut it out and shipped it as Feedsweep.
The frontend got a polish pass: virtualized sidebar with section headers and keyboard navigation, optimistic interactions, a full settings dialog, and React Compiler taking over manual memoization.
Hooks architecture and Feedmatch
This month I built an internal hooks system to separate the core reader logic from platform-specific behavior. The feed processing pipeline now exposes 20+ named lifecycle points where extensions can hook in. YouTube and GitHub integrations, logging, metrics — all of them moved out into standalone extensions on top of the same machinery.
Down the line, the same surface could be opened up so anyone can write their own extensions for Feedstand.
As somewhat of a tradition now, I've decided to extract the item deduplication logic into a separate package to make it more manageable. I've released it as Feedmatch.
The frontend got another push: OPML import with a progress dialog, infinite-scroll pagination, channel favicons, and a per-source layout system with cascading overrides.
On the API side: rate limiting, auth on feed routes, a per-user source cap, and ID encryption so internal numeric IDs don't leak in URLs.
Frontend takes shape
Now that the backend part is stable and robust, it's time for me to build the actual reader interface.
Since Feedstand is meant to be a traditional feed reader in the UI sense, I've built the sidebar, reader view with split-pane, source management, sign in and sign up, mark-as-read, starring, and tags.
I also rewrote item deduplication from scratch. The old approach matched items on a single identifier and kept quietly falling apart in real feeds — publishers change guids between fetches, the same item shows up under several identifiers, and what counts as "the same item" varies between sources. The new logic OR-matches across guid, link, title-with-date, and a normalized identifier hash.
Extracting Feedcanon
Another month, another new package. I've extracted the canonical URL resolution logic into a standalone library which I called Feedcanon.
The same feed is often accessible through different URLs — mixed schemes, trailing slashes, query params. The logic for collapsing those duplicates has been growing inside Feedstand for a while.
The more I worked on it, the more it looked like a problem worth solving in isolation, the same approach as Feedsmith and Feedscout earlier.
Feedcanon now resolves a candidate URL down to its canonical form before subscription, so duplicates collapse into a single feed instead of slipping through as separate ones.
On the internal tooling side, the project migrated fully from npm to Bun, took a shot at upgrading Drizzle to 1.0 beta, and removed a pile of unused packages.
Performance work and Feedscout
I got back to Feedstand after the Feedsmith push over the past few weeks.
Went deep into improving the performance across the entire feed processing pipeline (what actually gets and stores the content from feeds). It is now significantly faster and uses less memory, enabling the processing of more feeds in the same amount of time.
I also decided to extract the feed discovery logic into a new package, Feedscout. As a separate project, it gets more room to grow and to support various methods of feed discovery. All while keeping the feed reader codebase lean.
Work on Feedsmith continues
Over the last few months, Feedstand sat untouched as I dedicated all my time to Feedsmith. What started as a feed parser can now generate feeds, support OPML, and handle many feed namespaces.
Feedsmith is now effectively production-ready. It feels great to build a library that is faster and robust than many alternatives.
Time to bring it back into Feedstand and finally replace the internal feed handling.
Building the feed parser
I've decided to move away from rss-parser. It can't handle the variety of real-world feeds and it's quite slow. Rather than keep patching around its limitations, I started building my own parser.
This is how Feedsmith came to be. It's designed from the ground up to handle the mess that real-world feeds are. It's also very extensible to allow me to add more custom namespaces in the future. It got a nice reception on Hacker News, too.
Now, a general note. The more I work on Feedstand, the more I realize that a feed reader is more like a stack of separate problems pretending to be one project: fetching, parsing, discovery, content cleanup.
Scaling and feed engine
Came back to the project after a three-month break. I've spent a week or so on the deployment plumbing: CI for Docker images, routing, database migrations, plus a reworked fetch layer with conditional requests so unchanged feeds get skipped. Date parsing keeps growing in scope… feeds use every imaginable format.
As usual at the start of each project, you need to think about scaling it! I've configured multiple worker replicas, database connection pooling, memory limits, and job queuing. I also implemented basic item deduplication via content checksums, and added a feed fix suggestions system that flags broken channels and proposes corrections.
The system now reliably scans thousands of feeds across 2 replicas of the same Docker container of the backend.
Project bootstrap
After contemplating the idea of creating my own feed reader for a while, I finally took the plunge. I've set up the React frontend and the monorepo plumbing, and gone through three backend frameworks before settling on Hono.
I spent a big part of the time on the feed engine: parsing XML-based and JSON feeds, discovering them from arbitrary URLs (including YouTube). Real-world feeds turn out to be much messier than I expected. They feature non-standard date formats, numerous namespaces, and feeds that technically violate the specifications yet still need to function. Each one needs its own little patch, and the pile of custom treatments is already starting to feel like its own thing.
The core pipeline now runs end to end: discover a feed, subscribe to it, scan for new items on a schedule, and serve them as API responses.
