Introduction
There's a particular kind of guilt that accumulates in browser tabs. Each one represents an article someone recommended, a newsletter that looked interesting, a long-form piece that demands more attention than a quick scroll can provide. The tabs multiply. Some stay open for weeks. Most eventually close, unread, when the browser crashes or the shame becomes unbearable.
This pattern has repeated itself for as long as the web has existed. The solutions that emerged—bookmarks, read-it-later apps, RSS readers—all promised to solve the problem. Some did, for a while. But they all shared a fundamental assumption: that reading happens when you're sitting in front of a screen with time and attention to spare. That assumption has become increasingly divorced from how most people actually live.
The secret that changed everything for me was simple: most of my potential reading time isn't reading time at all. It's commuting time, cooking time, walking the dog time, doing laundry time. My eyes are occupied, but my ears are free. The twenty-minute newsletter becomes a companion during the morning run. The backlog of Substack posts becomes a podcast feed during the evening commute.
Text-to-speech has crossed a threshold that makes this possible. The robotic voices of a decade ago have given way to neural networks trained on thousands of hours of human speech. ElevenLabs, founded in 2022, reached a billion-dollar valuation by 2024 because they cracked the problem of making synthesized speech sound genuinely human. Speechify built an empire on the same insight applied to accessibility and education. Even browser-native speech synthesis has improved dramatically, especially on Apple devices with premium voices that sound startlingly natural.
But getting content into a TTS-friendly format remains harder than it should be. Web pages are polluted with navigation menus, newsletter signup forms, related article links, and the endless detritus of engagement optimization. Feed that to a text-to-speech engine and you get garbage audio—your relaxing article interrupted by someone reading aloud a cookie consent banner.
A personal reading library solves this problem by extracting just the content you care about, cleaning it thoroughly, and presenting it in formats optimized for both reading and listening. The same article that exists as a cluttered web page becomes clean text flowing through your earbuds.
The death of Google Reader in 2013 marked a turning point for how we think about personal content management. Google's shutdown wasn't driven by declining usage—internal sources suggest the product was successful and growing. It was sacrificed to push users toward Google Plus, and when it died, it took much of the RSS ecosystem with it. Mozilla removed RSS support from Firefox in 2018. Apple stopped providing RSS for Apple News in 2019. The open web gave way to algorithmic feeds owned by platforms.
But RSS never actually died. It persisted in the background, stubbornly refusing to disappear despite every obituary written for it. Substack, the platform that revitalized long-form newsletters, exposes RSS feeds for every publication. WordPress sites have had RSS for decades. The feeds still work, even if they're no longer advertised. And a new generation of tools—Feedly, NewsBlur, Readwise Reader—has emerged to serve people who want to control their information diet rather than surrender it to algorithms.
Pocket, one of the original read-it-later services, announced it would shut down in July 2025. Another reminder that building on platforms means accepting their timeline. When you build your own reading library, you own it. No subscription fees. No service shutdown announcements. No pivot to AI that deprecates the features you actually use.
Vibe coding makes building such a tool feasible for anyone with a clear vision of what they want. The components are well-documented: RSS parsing libraries, content extraction algorithms, database patterns for content storage, web APIs for text-to-speech integration. What previously required weeks of development compresses into sessions measured in hours. You describe the feature you want, see it materialize, refine it until it works.
The techniques that emerged from building a personal reading library with AI assistance reveal patterns that apply far beyond this specific project. There's something clarifying about building tools for yourself—you know exactly what success looks like because you're the user. The feedback loop tightens to nothing. You save an article, read it, notice something that annoys you, fix it immediately, and move on.
This book documents that process. Not as a tutorial to copy step by step, but as a map of the territory. The specific technologies matter less than the patterns: how to approach content extraction, what makes typography readable, how offline synchronization actually works, why certain TTS approaches succeed where others fail. The implementation details belong to your sessions with Claude. The concepts belong here.
By the end, you'll understand how to build a complete personal reading library: content discovery through RSS feeds, article extraction and cleaning, a reader interface with proper typography, text-to-speech integration that actually sounds good, organization through tags and reading lists, and deployment patterns that let you access your library from anywhere. More importantly, you'll understand the vibe coding techniques that make building such systems practical—the prompts that work, the patterns that emerge, the approach to iteration that turns vague ideas into working software.
The browser tabs will still accumulate. But now they'll have somewhere to go.
Project Setup
The temptation when starting any software project is to over-engineer from the beginning. Years of industry experience teach developers to anticipate scale, plan for edge cases, design for extensibility. These instincts serve teams building products for millions of users. They actively harm solo developers building tools for themselves.
A personal reading library has exactly one user. That user knows what they want because they're the person building it. The architecture should reflect this reality: simple enough to understand completely, flexible enough to modify when needs change, robust enough to not break during daily use.
The stack that emerged from my vibe coding sessions was deliberately boring. Node.js for the backend because JavaScript runs everywhere and TypeScript provides enough type safety to catch obvious mistakes. PostgreSQL for the database because full-text search capabilities matter for a reading library and Postgres handles this natively. A simple Express server for the API because the ecosystem is mature and well-documented. Vite for the frontend because it's fast and gets out of the way.
No microservices. No message queues. No distributed caching. No container orchestration. Just a monolithic application that runs on a single machine and does one thing well.
The architecture follows a pattern that repeats across vibe-coded projects: layers that separate concerns without introducing unnecessary complexity. The database stores everything that needs to persist. The services layer contains the business logic—parsing feeds, extracting content, generating speech. The API layer exposes endpoints for the frontend. The frontend renders data and captures user input. Each layer has a clear responsibility, and changes in one layer rarely ripple into others.
The database design deserves particular attention because it shapes everything else. Content applications live and die by their data models. Get the schema right early and the rest flows naturally. Get it wrong and you'll spend more time fighting your data than working with it.
A reading library needs to track feeds you've subscribed to, articles from those feeds, tags for organization, reading lists for curation, and progress through each article. That's five core entities with a few junction tables for many-to-many relationships. The temptation to add more—user accounts, sharing features, social integrations—should be resisted. Every table you add is complexity you'll maintain forever. Start minimal.
The vibe coding technique that worked best for database design was describing the domain in plain English and asking Claude to generate a schema. Not starting from the schema itself, but from what the application needs to do. Store articles with their full content and plain text. Track which feed each article came from. Allow multiple tags per article. Remember where I stopped reading or listening. From descriptions like these, Claude generates SQL that captures the relationships correctly.
UUIDs for primary keys turned out to be worth the minor overhead. When building offline-capable applications, the ability to generate identifiers on the client matters. You can save an article locally, assign it an ID, and sync that ID to the server later without conflicts. Auto-incrementing integers require a round-trip to the database to get an ID, which breaks in offline scenarios.
Storing both HTML and plain text for article content feels redundant until you understand why. The HTML version gets rendered in the reader view, preserving formatting that aids comprehension. The plain text version feeds into search indexes and text-to-speech engines. Computing plain text from HTML on every request would work but adds latency where it's least wanted. Storage is cheap. Time is not.
TypeScript configuration affects how the entire project evolves. The strict flags that matter most are the ones that prevent subtle bugs: requiring explicit return types when inference fails, treating array access as potentially undefined, distinguishing between missing properties and properties that might be undefined. These settings slow you down slightly when writing code and save hours when debugging.
The project structure follows convention rather than inventing something new. Source code in a source directory, tests alongside the code they test, database migrations in order, shared types in a central location. Anyone familiar with Node.js projects can navigate this structure without documentation.
Development workflow matters for vibe coding. The feedback loop between making a change and seeing its effect should be as short as possible. Hot reloading for the frontend. Watch mode for the backend that restarts on file changes. A database running locally in Docker so you can tear it down and rebuild whenever the schema evolves.
The technique that accelerated development most was treating the initial setup as a vibe coding session itself. Rather than manually configuring each tool, describe the development environment you want: TypeScript with strict settings, Express with standard middleware, PostgreSQL with Docker, Vite for the frontend. Claude generates the configuration files, and you iterate until everything runs. The specific settings matter less than having working tooling quickly.
One pattern that emerged repeatedly was the value of placeholder implementations. When building the project structure, stub out the services and endpoints even before they do anything real. A feed service that returns an empty array. An article endpoint that returns a hardcoded object. These placeholders establish the shape of the system and let you build the frontend against something real. You can vibe code the actual implementations later, confident that they'll slot into place.
The architecture decision with the most long-term impact was choosing to store article content locally rather than fetching it on demand. Some read-it-later applications work by saving URLs and extracting content when you view them. This approach fails for content that changes or disappears. It fails when you're offline. It fails when the original site restructures. A personal reading library should be more like a personal library: once you've saved something, it belongs to you, independent of whether the source continues to exist.
Testing strategy for personal tools differs from testing strategy for production applications. You don't need comprehensive coverage of every edge case. You need confidence that the core workflows won't break when you make changes. For a reading library, that means testing that feeds parse correctly, articles extract successfully, and search returns expected results. The tests serve as documentation as much as verification—a record of how the system should behave.
The foundation establishes constraints that make future development easier. When you add RSS feed support, you know where the service goes and how the API exposes it. When you build the reader interface, you know what data it will receive. When you integrate text-to-speech, you know the text content is already extracted and waiting. Each feature builds on the foundation rather than fighting against it.
Getting the architecture right means getting it simple. Complexity can always be added when specific needs demand it. Starting complex means carrying weight that slows every subsequent step. A reading library for yourself needs to work reliably and be enjoyable to modify. Everything else is overhead.
Content Discovery with RSS
Dave Winer started publishing an XML version of his blog in December 1997. The format spread to other sites, evolving through several versions before stabilizing as RSS—Really Simple Syndication. For a brief golden era in the mid-2000s, RSS represented a genuinely decentralized approach to web content. Subscribe to a feed, see everything published to it, no algorithm deciding what deserves your attention.
That era ended, or seemed to end, when Google Reader shut down in 2013. The shutdown wasn't about declining usage—engineers who worked on the product said it was successful and growing. It was sacrificed to push users toward Google Plus, a social network that never gained traction and eventually died itself. But the damage was done. Without Google Reader to educate users about RSS, adoption plummeted. Browsers removed RSS features. Platforms stopped promoting their feeds.
Yet RSS survived. Not as a mainstream technology, but as infrastructure that quietly powers much of the web. Every Substack publication has an RSS feed at a predictable URL. WordPress sites generate feeds automatically. Podcasts are distributed via RSS. The protocol refuses to die because it solves a real problem: letting people subscribe to content without surrendering control to a platform.
Building content discovery for a personal reading library means building on RSS. The alternative—scraping websites directly—is fragile, legally questionable, and constantly breaking. RSS feeds are explicitly designed for machine consumption. They follow documented formats. They represent a contract between publisher and subscriber: you can have this content in a structured format.
The vibe coding approach to RSS integration starts with describing what you want: a service that takes a feed URL, fetches it, parses the XML, and returns structured data about the feed and its items. Claude generates a feed service using established libraries like rss-parser that handle the parsing complexities. The first iteration usually works for simple feeds.
Complications emerge with real-world feeds. Some feeds are Atom instead of RSS, using slightly different XML structures. The parsing library handles both, but you learn to normalize the differences in your data model. Some feeds include full article content. Others provide only excerpts, requiring a separate fetch to get the complete text. Some feeds haven't been updated in years and return stale data. Others update constantly with hundreds of new items.
The pattern that worked best was capturing everything the feed provides while flagging anomalies for investigation. Don't reject feeds that deviate from expectations. Store what you get. When something looks wrong—a feed with no items, a feed that fails to parse—log the error and continue. The scheduler will try again later.
Substack feeds deserve special attention because Substack has become the dominant platform for long-form newsletters. Every Substack publication follows the same URL pattern: the publication name followed by the standard feed path. Given a Substack URL, you can construct the feed URL programmatically. The technique for adding this to your library is describing the pattern to Claude: given a URL that contains the Substack domain, extract the publication name and construct the feed URL.
Feed discovery extends beyond explicit RSS URLs. Many websites include link elements in their HTML that point to their feeds. A discovery service can fetch a webpage, parse the HTML looking for these link elements, and return the feeds it finds. This works for sites that have feeds but don't advertise them prominently. The technique involves describing HTML parsing to Claude: fetch this URL, find link elements with RSS or Atom types, extract the href attributes.
The scheduler that fetches feeds periodically required more thought than expected. The naive approach—fetch all feeds every thirty minutes—works until you have enough feeds that the fetches take longer than thirty minutes. Better to fetch feeds independently, tracking when each was last updated and prioritizing feeds that haven't been fetched recently.
Error handling in the scheduler became its own mini-project. Feeds fail for many reasons: network timeouts, invalid certificates, rate limiting, servers that are down. The scheduler needs to record these errors without crashing, continue processing other feeds, and retry failed feeds with backoff. This is exactly the kind of tedious, important work that vibe coding handles well. Describe the error handling behavior you want, let Claude generate the implementation, test it against real failure scenarios.
OPML emerged as an important format for feed management. OPML is an XML format that stores lists of RSS feeds, commonly used to export and import subscriptions between RSS readers. Supporting OPML import means users can migrate their existing subscriptions. Supporting OPML export means users can leave for another reader without losing their feed list. Both are straightforward to implement once you understand the format—Claude can generate parsers and generators from format descriptions.
The discovery interface in the frontend brought its own challenges. Users expect to paste any URL and have the system figure out the feed. They shouldn't need to know RSS exists. The interface should accept a blog URL, a Substack URL, or a direct feed URL, discover what's available, and present options to subscribe. This required chaining the discovery logic: try to parse the URL as a feed directly, try to discover feeds from the HTML if that fails, try common feed paths like slash feed or slash rss if discovery returns nothing.
Feedback during discovery matters for user experience. Network requests take time. The interface should indicate that discovery is happening, show results as they become available, and provide clear error messages when discovery fails. The vibe coding approach here was describing the user experience rather than the implementation: show a loading state while checking for feeds, display found feeds with subscribe buttons, show a helpful message if no feeds are found.
One technique that proved valuable was building the discovery features incrementally rather than all at once. Start with the simplest case: the user provides a direct feed URL, you fetch and subscribe. Then add Substack URL detection. Then add HTML discovery. Then add common path probing. Each addition is a focused vibe coding session that extends existing functionality. The alternative—trying to build comprehensive discovery from the start—leads to sessions that sprawl and stall.
The feeds table in the database accumulated fields over time as edge cases emerged. Last fetched timestamp for the scheduler. Fetch error field to record failures. Active flag to pause feeds without deleting them. Site URL and favicon for display. Each addition came from encountering a real need while using the library.
Content discovery is the foundation everything else builds on. Without feeds flowing into the system, there's nothing to read, nothing to organize, nothing to convert to speech. Getting discovery right—reliable, automatic, error-tolerant—determines whether the library becomes a daily tool or an abandoned project. The investment in solid feed handling pays compound interest across every other feature.
Content Ingestion and Storage
The modern web page is a hostile environment for reading. The article you want to read occupies perhaps forty percent of the screen, surrounded by navigation bars, newsletter signup forms, related article recommendations, social sharing widgets, comment sections, cookie consent banners, and advertisements that shift the layout as they load. This is not an accident. Every element exists because someone decided it served the business. The reader's experience is, at best, a secondary concern.
Content extraction is the art of finding the article within the page and discarding everything else. This turns out to be surprisingly difficult. There's no standard way to mark article content in HTML. Different websites use different element structures. Some wrap articles in article tags. Others use divs with class names like content or post-body. Some sites place the headline in an h1 element. Others use h2, or a div with a class that suggests heading-ness.
The algorithms that work best for content extraction use heuristics rather than rules. They look at the density of text versus HTML. They examine the structure of the page to find the largest continuous block of content. They identify patterns that suggest navigation or footer content and exclude those regions. Mozilla's Readability library, which powers Firefox's reader mode, has accumulated years of these heuristics and handles the majority of web pages correctly.
Using Readability from a vibe coding session is straightforward: describe wanting to extract article content from HTML, and Claude will generate code that imports the library and applies it. The result includes the cleaned HTML, the plain text, the title, the byline, and an estimated word count. Most of the time this just works.
Complications emerge with real-world feeds. Some sites load content dynamically with JavaScript, meaning the initial HTML contains none of the article text. Others embed content in iframes. Some use non-standard markup that confuses the extraction algorithms. These cases require individual attention—either fetching the page with a headless browser that executes JavaScript, or writing site-specific extraction rules.
The vibe coding technique for handling extraction failures was building a fallback chain. First try Readability. If that produces suspiciously short content, try an alternative extraction approach. If everything fails, store the raw HTML and flag the article for manual review. This defensive approach means the library continues to function even when individual articles fail to extract correctly.
Text cleaning goes beyond content extraction. The extracted HTML often contains elements that make sense visually but not when read aloud. Embedded tweets. Figure captions. Pull quotes that duplicate text from the article. Image descriptions that interrupt the flow. Cleaning this content for text-to-speech requires different rules than cleaning for display.
The approach that worked best was maintaining two versions of the cleaned content. The display version keeps visual elements that aid comprehension. The TTS version strips them aggressively. When you read an article, you see the version with images and pull quotes. When you listen to it, you hear just the prose. This dual-track approach emerged from using the library and noticing where the TTS experience suffered.
HTML sanitization addresses security concerns that casual developers often overlook. Extracted content might contain scripts, event handlers, or other potentially dangerous elements. Even without malicious intent, inline styles and data attributes clutter the content unnecessarily. A sanitization layer strips everything except the tags and attributes necessary for displaying text content: paragraphs, headings, links, images, lists, and basic formatting.
The allowed tags list started minimal and grew as needs emerged. Paragraph and heading elements obviously. Links to let readers follow references. Images to understand visual content. Lists for structured information. Block quotes for citations. Code elements for technical articles. Tables for structured data. Each addition came from encountering a real article that looked wrong without that element.
Links deserve special handling. In the browser, links open in new tabs to avoid navigating away from the reading experience. Links in TTS should probably not be spoken at all—hearing someone say h-t-t-p-s colon slash slash destroys the listening experience. The cleaning function for TTS strips URLs entirely, relying on the reader to check the display version if they want to follow references.
Reading time estimation uses a simple calculation: divide word count by typical reading speed, round to the nearest minute. This seems trivial until you consider that reading speed varies dramatically. Research suggests average adult reading speed around 250 words per minute, but actual speed depends on content complexity, reader familiarity with the subject, and the reading environment. Rather than optimizing for accuracy, the estimate provides a useful heuristic. A five-minute article versus a thirty-minute article helps you decide whether you have time to read it now.
Listening time differs from reading time. People comprehend spoken content at different rates than written content. The typical TTS playback speed of one-x matches roughly 150 words per minute, but many listeners prefer faster speeds—1.5x or 2x—which compress listening time proportionally. The library tracks listening time separately, adjusting based on the user's preferred playback speed.
Metadata extraction complements content extraction. Author names appear in bylines, meta tags, or JSON-LD structured data. Publication dates appear in various formats across different sites. Featured images appear in og:image meta tags or as the first image in the content. The extraction layer captures this metadata when available, falling back to sensible defaults when not.
The vibe coding approach to metadata extraction worked well because the patterns are documented but tedious to implement. Open Graph tags follow a standard format. JSON-LD uses a known schema. Time elements have predictable datetime attributes. Describing these patterns and asking Claude to generate extraction code produces comprehensive coverage without manually writing every case.
URL normalization prevents duplicate articles from accumulating. The same article might be shared with different tracking parameters—utm_source, utm_medium, and friends. The same article might appear at both www and non-www versions of a domain. The same article might use http and https protocols. Normalizing URLs before storing them ensures that different versions of the same link don't create duplicate entries.
The technique for URL normalization started simple and grew more sophisticated through use. Strip tracking parameters. Lowercase the hostname. Remove trailing slashes. Handle the edge cases as they appear—some sites include session identifiers in URLs, some use fragment identifiers for tracking. Each edge case gets handled when it causes a duplicate, not preemptively.
Content deduplication extends beyond URL matching. Sometimes the same article appears at multiple URLs—syndicated across sites, reposted under different URLs, or moved during site restructuring. Content fingerprinting catches these cases by hashing a sample of the article text. Two articles with the same fingerprint are probably the same article, regardless of their URLs.
The fingerprinting algorithm doesn't need to be cryptographically secure or perfectly accurate. It needs to catch obvious duplicates without generating false positives. Hashing the first thousand characters of normalized text—lowercase, whitespace collapsed, punctuation stripped—works well enough for practical use.
The article service ties all these pieces together into a coherent workflow. Receive a URL. Check if we already have it. Fetch the page. Extract content. Clean the HTML. Generate plain text. Calculate reading time. Store everything. The service provides a clean interface that hides the complexity: give it a URL, get back an article.
Manual article saving adds another path into the library. Beyond subscribed feeds, users want to save arbitrary URLs—articles shared on social media, links from email newsletters, pages discovered while browsing. This save functionality triggers the same extraction pipeline as feed processing, but initiated by user action rather than scheduled fetching.
The bookmarklet approach provides one-click saving without browser extension complexity. A bookmarklet is a small piece of JavaScript that lives in the browser's bookmarks bar. Click it, and it sends the current page URL to your library's API. The library handles extraction on the server side. This approach works in any browser without installation.
Vibe coding the bookmarklet involved describing the user experience: capture the current URL, send it to a specific endpoint, show a confirmation. Claude generated the minified JavaScript suitable for a bookmarklet. The result is ten lines of code that provides one-click article saving from any browser.
Error handling during extraction follows the same philosophy as error handling during feed fetching: don't fail completely, don't lose data, flag problems for investigation. If Readability throws an exception, catch it, store what information you have, mark the article as needing review. The library should continue functioning even when individual extractions fail.
Testing extraction is inherently fuzzy. Unlike unit tests with deterministic inputs and outputs, content extraction produces results that are correct in degree rather than absolutely. This article extracted perfectly. That article extracted mostly correctly but lost a sidebar quote. Another article failed completely. Testing focuses on the important cases: does extraction work for the sources you actually read? Capture real articles from your feeds, verify they extract correctly, and add regression tests for articles that revealed bugs.
The extraction pipeline transforms the chaos of the web into clean, structured content ready for reading and listening. Getting this right determines whether the library feels magical or frustrating. When extraction works, you forget it exists—you just read the articles. When extraction fails, every failure interrupts your reading flow and reminds you that you're using software rather than a library.
Building the Reader Interface
Good reading typography is invisible. You notice it only when it fails—when your eyes strain after twenty minutes, when you lose your place at the end of each line, when the spacing feels cramped or wasteful. The best reader interfaces create the sensation that you're reading a well-printed book, not staring at a glowing rectangle.
Typography for screens has matured dramatically since the early web. We've moved from the Times New Roman versus Arial debates of the nineties through the web font revolution of the 2010s to today, where system font stacks provide beautiful, fast-loading typography without external dependencies. The techniques that seemed like professional secrets a decade ago are now best practices anyone can apply.
The foundation is the font stack. A well-constructed font stack specifies your preferred fonts in order, falling back gracefully when fonts aren't available. For reading, serif fonts generally outperform sans-serif—the serifs guide the eye along the baseline, reducing cognitive load over extended reading sessions. But the specific fonts matter less than consistency. Pick a stack and apply it everywhere.
The vibe coding approach to typography worked surprisingly well. Describe the reading experience you want: a clean, book-like presentation with generous line height, optimal line length, and subtle color that's easy on the eyes. Claude generates CSS that implements these principles. The first iteration is usually close; refinement through use produces excellent results.
Line length deserves particular attention because it affects reading comprehension more than most people realize. Research consistently shows optimal reading at fifty to seventy-five characters per line. Too short and the eye makes excessive movements. Too long and readers lose their place moving to the next line. The magic number that emerged from my testing was sixty-five characters, achieved by constraining the content width using the ch unit that represents the width of the zero character.
Line height complements line length. Dense text with minimal spacing creates the cramped feeling of a technical manual. Text with too much spacing feels airy and disconnected. The sweet spot for body text falls around 1.6 to 1.8 times the font size. I settled on 1.7 after comparing against actual books I enjoy reading.
Font size on screens requires thinking differently than font size in print. A sixteen-pixel font that's perfectly readable in a code editor feels cramped when reading long-form prose. Eighteen pixels became my baseline, with user settings allowing adjustment up or down. The human eye varies, and letting readers customize prevents the font size from becoming a barrier to using the library.
Color choices affect both aesthetics and eye strain. Pure black text on pure white backgrounds creates harsh contrast that fatigues eyes over extended reading. The trick is softening both: off-white backgrounds with very dark gray text. The difference is subtle—most people don't consciously notice—but the cumulative effect over an hour of reading is significant.
Dark mode is no longer optional. Many readers prefer dark backgrounds, whether for aesthetic preference or genuine light sensitivity. Supporting both light and dark modes means defining colors as CSS custom properties that change based on a theme attribute or media query. The system preference detection uses the prefers-color-scheme media query, which respects the user's operating system settings automatically.
The approach that worked best was providing three options: light, dark, and system. System mode follows the OS setting, automatically switching when the user's device switches. Explicit light and dark modes let users override the system when they have a specific preference. This three-way toggle handles every reasonable user expectation.
The reader view itself combines the typography choices into a focused reading environment. The article title displays prominently at the top. Metadata—author, publication date, estimated reading time—appears below in secondary styling. The article content fills the central column. Footer elements provide actions like favoriting, archiving, or viewing the original.
Progress tracking transformed how I used the library. Knowing where you stopped reading, and being able to resume from that point, turns occasional reading into a continuous experience. The implementation tracks scroll position as a percentage of document height, saves it periodically while reading, and restores it when reopening the article.
The scroll tracking technique required careful design to avoid performance problems. Scroll events fire frequently—potentially hundreds of times during normal reading. Saving to the server on every scroll event would overwhelm both the browser and the API. Instead, debouncing the save operation ensures it only fires after scrolling stops for a moment. The position updates in memory immediately but only persists after a pause.
Marking articles as read happens automatically when the reader scrolls past a threshold—I use ninety percent of the article. This eliminates the tedious step of manually marking things read while still preserving accuracy. If you opened an article and closed it immediately, it stays unread. If you read through to the end, it marks itself read.
The article list view serves as the library's front door. It shows available articles with enough information to make reading decisions: title, excerpt, author, publication date, estimated reading time. Unread articles appear more prominent than read ones. Filters let users focus on unread items, favorites, or specific feeds.
Infinite scrolling proved more appropriate than pagination for article lists. Unlike pagination, which forces users to make navigation decisions, infinite scrolling lets users simply continue browsing. As they approach the bottom, more articles load automatically. This feels more natural for a reading queue than explicit page navigation.
The list-detail navigation pattern that emerged matches how people actually read. Browse the list, tap an article, read it, go back to the list. The browser's history API supports this without full page reloads—navigation feels instant because only the content changes, not the entire page.
Reader settings let users customize their experience beyond the defaults. Font size adjustment helps users with different vision needs. Font family choice between serif and sans-serif respects personal preference. Line height adjustment accommodates different reading styles. Theme selection provides explicit control over light and dark modes.
Settings persistence uses local storage, the simplest approach that actually works. Values save immediately when changed. They load when the application starts and apply before the page renders, preventing the flash of unstyled content that occurs when settings apply after the page is already visible.
The vibe coding technique for building the reader interface was describing the user experience rather than the implementation details. What should happen when someone opens an article? What should they see? How should navigation work? Claude generates the component structure and state management to make that experience happen. The iteration process involves using the reader, noticing friction, describing what should be different, and regenerating.
Testing the reader interface is mostly visual verification. Does the typography look good at different screen sizes? Does dark mode contrast appropriately? Does the progress bar update smoothly while scrolling? These questions don't have automated test answers—they require human judgment informed by actually using the interface.
The mobile experience required specific attention. Touch targets need minimum sizes for reliable tapping. Text needs to remain readable without zooming. Progress indicators need to account for virtual keyboards that change available viewport height. These concerns don't affect desktop usage but determine whether mobile reading is pleasant or frustrating.
Accessibility considerations shaped several design decisions. Semantic HTML ensures screen readers can navigate the content. Sufficient color contrast helps users with visual impairments. Keyboard navigation allows readers who don't use mice. Focus indicators show where keyboard focus currently sits. These features benefit everyone while being essential for some users.
The reader interface is where users spend most of their time with the library. Getting it right—not just functional but genuinely pleasant to use—determines whether the library becomes a daily tool or an abandoned project. The investment in typography, smooth interactions, and customization options pays dividends every time you read an article.
Text-to-Speech Integration
The breakthrough that transformed my reading library from useful to indispensable was adding text-to-speech. Not as a secondary feature or accessibility accommodation, but as a primary mode of consuming content. The articles I never had time to read became the articles I listened to during commutes, workouts, and household chores.
Text-to-speech technology has undergone a revolution that most people haven't fully registered. The robotic voices of a decade ago—the ones that mispronounced common words and paused in strange places—have given way to neural network-generated speech that sounds remarkably human. ElevenLabs, founded in 2022, demonstrated what was possible and reached a billion-dollar valuation by 2024. Speechify built a successful business on making TTS accessible for education and accessibility use cases. The technology crossed a threshold from curiosity to utility.
Even browser-native speech synthesis has improved dramatically. The Web Speech API, supported in all modern browsers, provides access to high-quality voices without external services. Apple devices include premium voices that sound nearly indistinguishable from human speech. The quality varies by operating system and browser, but the worst modern TTS is better than the best TTS was five years ago.
Integrating TTS into a reading library involves solving several distinct problems. First, getting the text into a format suitable for speech. Second, actually generating the audio. Third, building player controls that work naturally. Fourth, tracking position so listeners can pause and resume. Each problem has subtleties that become apparent only through use.
The text preparation problem turned out to be harder than expected. Content that reads well doesn't necessarily listen well. URLs are unpronounceable garbage when spoken aloud—hearing someone say h-t-t-p-s colon slash slash destroys any listening flow. Code blocks that make sense visually become incomprehensible when read sequentially. Embedded tweets and pull quotes that duplicate article content cause the same passage to be read twice.
The solution was maintaining separate text versions optimized for different purposes. The display version keeps all the visual elements that aid comprehension when reading. The TTS version strips aggressively: remove URLs, remove code blocks, remove redundant pull quotes, collapse excessive whitespace. The cleaning function became one of the most-revised pieces of code as edge cases emerged through actual listening.
The Web Speech API provides the simplest path to TTS integration. A few lines of JavaScript create an utterance, assign it a voice, and speak it. The API handles the conversion from text to audio. The browser manages the speech synthesis. For basic functionality, this approach works immediately.
Complications emerge with longer content. The Web Speech API in most browsers has quirks with extended text—some browsers stop speaking after fifteen seconds of continuous output. The workaround is chunking: break the text into segments, speak each segment sequentially, and manage the transitions between segments so they sound continuous.
The chunking strategy matters more than it might seem. Breaking mid-sentence creates awkward pauses that disrupt comprehension. Breaking at sentence boundaries sounds natural but can create very long chunks if sentences are complex. The approach that worked best was preferring sentence boundaries while enforcing a maximum chunk size, splitting mid-sentence only when necessary.
Voice selection significantly affects the listening experience. Each browser exposes different voices depending on the operating system. macOS includes high-quality voices developed by Apple. Windows has its own voice collection. Linux varies by distribution. Mobile browsers have different voice selections than desktop browsers. The library should discover available voices, filter to those with acceptable quality, and let users choose their preference.
Finding high-quality voices programmatically requires heuristics. Voice names that include words like "enhanced" or "premium" often indicate better quality. Voices marked as "local" rather than "network" avoid latency issues. The recommended voice selection function tries these heuristics, falling back to the first available English voice if nothing better is found.
Playback controls mirror what users expect from audio applications. Play and pause are obvious. Speed adjustment lets listeners compress or expand listening time—many people prefer 1.5x or 2x speed once they're accustomed to a voice. Skip forward and back by fixed intervals helps when attention wanders or something needs repeating. A progress indicator shows position through the content.
Position tracking for TTS differs from position tracking for reading. Scroll position measures visual progress through the document. Audio position measures progress through the text, which doesn't map directly to scroll position because text density varies. The library tracks both separately, restoring whichever is relevant when the user returns.
The hybrid approach that proved most valuable lets users switch between reading and listening within the same article. Start reading at your desk. Reach a good stopping point, switch to audio, and continue from where you were during the commute home. Return to reading that evening, picking up where the audio left off. This fluid transition between modalities is where the library stops feeling like software and starts feeling like a reading companion.
Premium TTS services offer substantially better voice quality than browser-native options. ElevenLabs provides voices that are nearly indistinguishable from human narration. Speechify's voices are optimized for extended listening. These services aren't free—they charge per character or per minute of generated audio—but for content you'll actually listen to, the quality improvement is worth considering.
Integrating premium services follows a different pattern than browser-native TTS. Instead of generating speech on the client, you send text to an API and receive audio back. This audio can be streamed for immediate playback or cached for offline listening. The network round-trip adds latency that browser-native TTS avoids, but the quality makes it worthwhile for content you care about.
The architecture that emerged supports both approaches. Browser-native TTS is the default, free, and works offline. Premium TTS is available when configured, used for content where quality matters. The user can choose per-article or set a default preference. This flexibility lets users optimize their own cost-quality tradeoff.
Server-side audio generation enables features that client-side TTS cannot. Generate the audio once, cache it, and serve the same audio to multiple playback sessions without regenerating. Convert articles to MP3 files that can be downloaded and played in any audio app. Pre-generate audio during off-peak hours so it's ready when users want to listen. These capabilities require more infrastructure but unlock workflows that pure client-side TTS cannot support.
The vibe coding technique for TTS integration involved describing the listening experience rather than the technical implementation. What happens when someone presses play? How does seeking work? What should the controls look like? Claude generates the speech synthesis code, the chunking logic, the player component. The iteration process involves actually listening to articles, noticing where the experience breaks, and describing what should be different.
Testing TTS is inherently subjective. Does the voice sound good? Are the pauses in the right places? Does the chunking cause noticeable breaks? These questions require human judgment. Automated tests can verify that chunking produces the expected number of segments or that the player component renders correctly, but they can't assess whether the listening experience is pleasant.
Edge cases in TTS reveal themselves through use. Abbreviations that should be expanded. Acronyms that should be spelled out. Numbers that should be spoken as words versus digits. Punctuation that affects pronunciation in unexpected ways. Each edge case requires adjustment to the text cleaning pipeline, accumulating handling for the specific patterns that appear in the content you actually consume.
The result is a reading library that reads to you. Not as a gimmick or accessibility feature, but as a genuine alternative to visual reading. The articles you saved but never read become the content you consume while doing other things. The reading backlog transforms from guilt to opportunity.
Organization and Search
A reading library grows quickly. Subscribe to ten feeds and you'll have hundreds of articles within weeks. Without organization, finding that article you half-remember becomes impossible. The reading list transforms from opportunity into overwhelming obligation.
Organization is the difference between a tool you use and a tool you abandon. The best organizational systems share a quality: they impose just enough structure to enable finding things without demanding so much effort that the organization itself becomes a burden. Too simple and you can't find anything. Too complex and you stop organizing.
Tags emerged as the primary organizational mechanism. A tag is just a name attached to articles that share something in common. Technology articles. Articles about writing. Articles to reference later. The specific tags don't matter—what matters is that they make sense to you and that applying them takes minimal effort.
The tagging interface needed to prioritize speed over comprehensiveness. When you save an article, a small tag selector lets you add existing tags or create new ones. The selector should autocomplete based on your existing tags, making common categorization a single keystroke. New tags create instantly without navigating away from the article.
Colors for tags seem trivial but significantly improve scanning. A colored dot next to each tag provides visual grouping that plain text cannot. The colors can be chosen explicitly or generated automatically from the tag name—a hash function that maps strings to hues produces consistent, distinguishable colors without requiring selection.
AI-assisted tagging proved more useful than expected. Claude can read article content and suggest appropriate tags from your existing collection. The suggestions aren't always right, but they're right often enough to speed up organization. A button to request suggestions, a list of recommended tags, checkboxes to accept or reject—the interface makes human judgment fast while letting AI do the initial categorization.
The vibe coding approach to building the tag service was describing the operations needed: create a tag, list all tags with usage counts, add tags to articles, remove tags from articles. Claude generates the database queries and API endpoints. The frontend components for tag management follow from describing how users interact with tags—selecting them, creating them, seeing which articles have which tags.
Reading lists provide a different organizational model than tags. Where tags categorize articles by content, reading lists curate articles for a purpose. The distinction matters. An article might be tagged "technology" and "business" based on what it contains. The same article might be added to a "Present to the team" reading list based on how you plan to use it.
Reading lists can be ordered. Tags typically appear alphabetically or by usage frequency, but reading lists let you sequence articles deliberately. The first article in the list is the one to read first. Drag-and-drop reordering makes sequencing intuitive. The position persists across sessions.
Combining tags and reading lists provides powerful filtering. Show articles with this tag that are also in this reading list. Show unread articles tagged for research. Show everything saved this week that matches a search query. The combinations emerge from the relational database structure—joins and filters compose naturally.
Full-text search transforms a growing library from burden to asset. PostgreSQL includes sophisticated full-text search capabilities that handle the vast majority of search needs without external search services. Articles become searchable by title and content. Searches return ranked results with matching excerpts highlighted.
The vibe coding technique for search involved describing the user experience: type a query, see matching articles ranked by relevance, show which part of each article matched. Claude generates the SQL that creates text search indexes, ranks results, and generates highlighted excerpts. The complexity hides behind a simple interface.
Search suggestions make finding specific articles faster. As users type, potential completions appear based on article titles. Selecting a suggestion navigates directly to that article. This typeahead functionality requires querying the database with partial strings, which PostgreSQL handles efficiently with the right indexes.
Related articles emerged as an unexpected discovery feature. Given an article you're reading, which other articles discuss similar topics? Full-text search can answer this by extracting keywords from the current article and searching for matches. The feature surfaces connections you might not have remembered making.
Archive functionality keeps the library clean without losing content. Archiving an article removes it from the default view—you won't see it in the unread list or when browsing feeds. But the article remains searchable and accessible. This differs from deletion, which removes the article permanently. Archive for things you're done with but might want to find later. Delete for things that should never have been saved.
Favorites mark articles for easy access. Unlike tags, which require naming a category, favoriting is a single action with no decision beyond "I want to find this easily." The favorites list becomes a personal best-of collection, curated through the simple act of pressing a button.
Filtering controls let users slice the library by multiple dimensions simultaneously. Show only unread articles. Show only favorites. Show only articles from a specific feed. Combine these with tag filters and search queries to find exactly what you're looking for. The filter interface needs to be discoverable without being overwhelming—sensible defaults with the ability to customize.
The organization features compound in value as the library grows. A library with fifty articles doesn't need sophisticated organization. A library with five thousand articles is unusable without it. Building organization features early means they're available when needed, rather than scrambling to add them after the library becomes unmanageable.
Testing organization features requires realistic data. A single article shows whether the tag selector renders correctly. A thousand articles reveal whether the full-text search returns results quickly enough. Testing at scale caught performance problems that didn't appear with small datasets—queries that executed instantly with a hundred articles took seconds with ten thousand.
The organization layer sits between raw content and human attention. It transforms an undifferentiated stream of articles into a structured collection that respects how you want to engage with content. Getting organization right means the library grows without becoming overwhelming. The oldest articles remain as findable as the newest ones.
Deployment and Sync
A reading library that only works on one computer isn't much of a library. The value multiplies when you can access your content from anywhere—starting an article on the desktop, continuing on the phone during commute, finishing on a tablet before bed. Deployment choices determine whether this fluidity is possible.
Personal tools have unique deployment considerations that differ from typical web applications. There's exactly one user. There's no need for multi-tenancy, complex authentication, or horizontal scaling. The privacy of your reading habits matters—what you read is genuinely personal information. Offline access isn't optional; you read in subways, on airplanes, in areas with poor connectivity.
The simplest deployment is local only: the application runs on your computer, the database lives on your computer, everything stays private and under your control. This works perfectly for reading at home. It fails the moment you want to read somewhere else.
Self-hosted deployment on a personal server—a VPS from DigitalOcean, Linode, or Hetzner—extends access beyond your local machine. The application runs on the server, accessible from any device with an internet connection. Your reading library becomes available from your phone, your work computer, anywhere with a browser.
Docker simplifies server deployment dramatically. Package the application and its dependencies into a container, define the database in a Docker Compose file, and the entire system deploys with a single command. The same container that runs locally runs identically on the server. No hunting down dependency mismatches or configuration differences.
The vibe coding approach to containerization worked well. Describe wanting to package the application for deployment, specify that you want Docker Compose to include both the application and Postgres, mention that you need SSL and a reverse proxy. Claude generates the Dockerfile, the Compose configuration, the Nginx setup. The details of layer caching and multi-stage builds—optimizations that experienced developers know—appear automatically.
Authentication protects a publicly accessible library from unauthorized access. For a single-user application, complex authentication systems are overkill. API key authentication—a long random string that must be included in requests—provides adequate security with minimal complexity. Generate a key, store it as an environment variable, check it on every request.
HTTPS is not optional for a server-hosted library. Let's Encrypt provides free SSL certificates with automated renewal. Certbot integrates with Nginx to handle the certificate management. The setup is a one-time investment that pays ongoing dividends in security and browser compatibility.
Offline support transforms the library from a web application into something closer to a native app. Service workers intercept network requests and serve cached content when the network is unavailable. IndexedDB stores article content locally, enabling offline reading even without server access. The offline experience should feel identical to the online experience except for the inability to sync new content.
The service worker caching strategy matters. For static assets—JavaScript, CSS, images—cache-first makes sense: serve from cache if available, only fetching from network when the cache is empty. For article content, network-first with cache fallback ensures you see the latest content when online while still functioning offline.
IndexedDB provides substantial local storage for offline articles. Unlike localStorage, which is limited to a few megabytes, IndexedDB can store hundreds of megabytes of article content. The offline storage service saves articles explicitly marked for offline reading, keeping them available regardless of connectivity.
Synchronization handles the gap between offline changes and server state. Mark an article as read while offline, and that change should sync when connectivity returns. The sync queue stores pending changes and processes them when the network becomes available. Conflict resolution—what happens when the same article was modified both offline and on another device—follows a simple rule: most recent change wins.
The sync service monitors online status and processes the queue when connectivity resumes. A background listener triggers synchronization automatically when the browser detects a network connection. The user doesn't need to think about syncing; it happens transparently.
Backup strategy protects against data loss. A reading library accumulates years of curated content, reading history, and organizational structure. Losing that data would be devastating. Regular backups to a separate location provide insurance against server failure, accidental deletion, or database corruption.
The backup service exports all data—feeds, articles, tags, reading lists, progress—to a JSON file that can be stored anywhere. The format is human-readable and simple enough to import into a replacement system if needed. Scheduled backups run automatically, pruning old backups to avoid unbounded storage growth.
The restore process reverses the backup: load the JSON file, clear existing data, insert the backed-up records. Testing restore is as important as testing backup—a backup you can't restore isn't really a backup.
Progressive Web App capabilities make the library feel more like a native application. Adding a web manifest enables installation to the home screen on mobile devices. The manifest specifies the app name, icon, and display mode. Once installed, the library opens without browser chrome, looking and feeling like a native app.
Cross-device state synchronization extends beyond just syncing changes. Position within an article, current reading list, TTS playback state—these should all follow the user across devices. The synchronization becomes invisible; you just pick up where you left off, regardless of which device you left off on.
The deployment architecture that emerged from my vibe coding sessions prioritizes simplicity and reliability over optimization. A single server running Docker Compose with the application and database. Nginx as a reverse proxy handling SSL. Service worker caching for static assets and offline article storage. Background sync for pending changes. Daily backups to off-site storage. This setup handles one user's reading library without complexity that serves no purpose.
Testing deployment requires testing the entire stack, not just individual components. Does the Docker container build correctly? Does the application start after a fresh deployment? Do SSL certificates renew automatically? Does the backup actually contain all the data? Does restore actually work? These integration tests catch problems that unit tests miss.
The result is a reading library that exists as infrastructure, always available and rarely thought about. You read articles. They sync. Backups happen. The technical complexity disappears behind the simple experience of reading.