A curated RSS feed reader built with Next.js and deployed on Vercel. Feed URLs are managed in a local config file and pushed to GitHub. The app is read-only and public — no authentication, no write operations from the public-facing side.
Production URL: https://rss-reader-three-omega.vercel.app
The repo name is still rss-reader for historical reasons; the site itself is branded "All Things AI".
- Next.js 14.x (Pages Router) — deliberately chosen to avoid CVE-2025-55182, a CVSS 10.0 RCE in React Server Components confirmed in the wild. Do not upgrade to Next.js 15.x or 16.x.
- TypeScript — compile-time type safety for feed parsing, config validation, and UI components
- Tailwind CSS — utility-first styling with no runtime overhead
- Vercel — deployment with automatic TLS, edge caching, and preview deployments
# Install dependencies
npm ci
# Start dev server
npm run devThe app runs at http://localhost:3000.
The easiest way to manage feeds is via the built-in admin UI. Start the dev server and navigate to http://localhost:3000/admin. A Manage Feeds link also appears in the sidebar in dev mode.
- Add — fill in name, URL (
https://required), and category - Edit — click Edit on any row to modify inline
- Delete — click Delete; confirms before removing
- Commit & Push — commits
feeds.public.jsonand pushes directly tomain, triggering a Vercel production deployment. The button is disabled until a change has been made.
The admin page and its API routes return 403 / "Not available" in production. The sidebar link is not rendered in production builds.
Edit feeds.public.json at the project root directly. Each entry requires name, url, and category:
[
{
"name": "Example Feed",
"url": "https://example.com/feed.xml",
"category": "Tech"
}
]The file is validated against feeds.schema.json by a pre-commit hook and in CI. Invalid changes will be rejected. After editing, commit and push to main to deploy.
For feeds you don't want visible in the repo:
- Create
feeds.private.jsonlocally (already in.gitignore) with the same format - On Vercel, set the
PRIVATE_FEEDSenvironment variable to the JSON string contents of your private feeds config via the Vercel dashboard — never set secrets directly in the terminal
Cards without a feed-supplied image show a black banner. To use a custom logo for a source:
- Add the logo image to
public/(e.g.public/Logo - My Source.png) - Add one entry to the
SOURCE_LOGOSmap insrc/components/ArticleCard.tsx:
const SOURCE_LOGOS: Record<string, string> = {
"Source Name As It Appears On Cards": '/Logo%20-%20My%20Source.png',
};The source name must match item.source exactly (visible in small text above each article title).
Currently configured:
| Source name | File |
|---|---|
| Lenny's Podcast | Logo - Lennys Podcast.png |
| Wyndo | Logo - AI Maker.png |
| The AI Daily Brief | Logo - AI Daily Brief.jpg |
| Hard Fork | Logo - Hard Fork.png |
| Practical AI | Logo - Practical AI.webp |
| AI in Business | Logo - AI in Business.png |
| Response Awareness Methodology | Logo - Response Awareness Methodology.webp |
| OpenAI News | Logo - OpenAI.jpg (feed not currently active) |
The home page and category pages use Incremental Static Regeneration (ISR) with revalidate: 300. Pages are pre-rendered at build time with feed data baked into the HTML, then regenerated every 5 minutes in the background. First-byte is served instantly from Vercel's CDN — no client-side fetch waterfall.
The /api/feeds endpoint (still used by the admin page and as a fallback) uses s-maxage=300 with an in-memory 60-second dedup cache.
Each feed is capped at 20 items (MAX_ITEMS_PER_FEED in src/lib/rss.ts). This prevents high-volume archives from dominating the combined feed — OpenAI's blog RSS, for example, returns 900+ items in a single fetch.
Feed descriptions pass through sanitize-html with a strict tag/attribute allowlist. Non-HTTPS src attributes on images are stripped.
Direct pushes to main are used for routine changes (feed edits, UI tweaks). Branch protection is in place but enforce_admins is false, so the repo owner can push directly.
For larger changes that should go through CI:
- Push changes to the
previewbranch - Open a PR from
previewtomain - Wait for CI to pass (feed validation, audit, type check, build)
- Merge the PR
- Verify the production deployment on Vercel
The preview branch is permanent — never delete it.
On every PR targeting main, GitHub Actions runs:
npm run validate-feeds— validatesfeeds.public.jsonagainst JSON Schemanpm audit --audit-level=critical— checks for critical vulnerabilitiesnpx tsc --noEmit— TypeScript type checkingnpm run build— production build
All steps must pass before merging.
| Variable | Where | Purpose |
|---|---|---|
PRIVATE_FEEDS |
Vercel dashboard only | JSON string of private feed configs |
Never set PRIVATE_FEEDS or any secret directly in the terminal. Use .env.local for local development and the Vercel dashboard for production.