Skip to content
Submitted 13 days ago

RSS/Atom feed reader with Next.js, TypeScript, Tailwind & Playwright

typescript, next, tailwind-css, playwright, vitest
P
LVL 3
@ZeroCool989
A solution to the RSS feed reader challenge

Solution retrospective


What are you most proud of, and what would you do differently next time?

I'm most proud of the feed parsing pipeline. It handles RSS 2.0, RSS 1.0/RDF and Atom with the mess real feeds actually throw at you: double-encoded entities, CDATA vs escaped HTML, Atom type="xhtml" content (which needs an order-preserving parse or paragraphs come out scrambled), truncated XML, timezone-less dates, and feeds that have simply died. I tested it against all 19 curated feeds live before writing any UI, and that decision paid off constantly.

I'm also glad I invested in a real test suite: 71 unit tests plus 30 Playwright e2e tests. Writing them caught six genuine bugs, including an SSRF bypass via hex-form IPv6-mapped addresses ([::ffff:7f00:1]) and 304 responses being mishandled as redirects. None of those would have surfaced from clicking around.

Next time I'd take the full-stack path from the start: real database, auth, and cross-device sync. I went frontend-only (localStorage + server-side fetching via API routes) to focus on the parsing and the reading experience, and while that's an officially supported path, the data layer is now the obvious ceiling. I'd also reach for list virtualization earlier: some feeds ship 1,300+ items and capped pagination only gets you so far.

What challenges did you encounter, and how did you overcome them?

Real feeds are hostile input. My XML parser's entity-expansion guard (a billion-laughs protection) rejected legitimate full-content feeds with more than 1,000 ordinary & entities. I had to disable entity processing and write my own decoder with separate handling for CDATA vs escaped content, so code samples in articles don't get eaten by the sanitizer.

SSRF protection has more edge cases than expected. Blocking localhost and 192.168.x.x is the easy part. The URL parser normalizes decimal (http://2130706433), octal and hex IPs, and IPv6-mapped addresses come out in hex form, which my first check missed. Every redirect hop needs re-validation too. Unit-testing the bypass table from my security checklist is what caught the hole.

Concurrency bugs that only appear under load. During e2e runs, an OPML import racing a guest boot fired 40+ simultaneous upstream fetches and produced spurious "could not reach" failures. Individually every feed worked. The fix was a DNS-verdict cache, a semaphore capping concurrent upstream fetches, and a single retry for transient network errors — all things a real deployment would have needed anyway.

Playwright strict mode. My article rows are role="button", so getByRole("button", { name: "Saved" }) happily matched an article whose headline contained "saved". Scoping queries to the sidebar and using exact: true fixed a whole class of flaky tests.

What specific areas of your project would you like help with?
  1. State architecture (src/lib/store.ts): I keep everything in one Zustand store and compute unread counts with selector functions called from components. With ~1,200 articles, is there a re-render cost I should address with memoized/subscribed selectors, or is this fine at this scale?

  2. localStorage strategy: I persist a trimmed article cache (25 per feed, no HTML content) for instant paint on revisit, with full content in memory only. Would you switch to IndexedDB for this, and is my pruning approach (capping read-state at 6,000 ids) sensible?

  3. Digest ranking (src/components/DigestView.tsx): it's currently recency-based — everything unread since your last visit, one hero story, five per category. What signals would you rank by without engagement data? Source weighting? Reading history?

  4. Keyboard navigation + screen readers (src/hooks/useKeyboard.ts): j/k moves a visual selection with data-selected. Should the selection also move DOM focus or use aria-activedescendant? I'd value input from anyone who's tested similar patterns with a screen reader.

  5. Feed list performance: infinite scroll with IntersectionObserver works, but for 1,000+ item feeds, would you virtualize (e.g. TanStack Virtual), and how would you handle the three different row heights across my layout modes?

Code
Loading...

Please log in to post a comment

Log in

Community feedback

No feedback yet. Be the first to give feedback on ZeroCool989’s solution.

Join our Discord community

Join thousands of Frontend Mentor community members taking the challenges, sharing resources, helping each other, and chatting about all things front-end!

Join our Discord