Resend RSS Feed Generator
The Origin Story: My friend M asked to be removed from our newsletter. But instead of just hitting unsubscribe like a normal person, he said: "Just give me an RSS feed". So here we are. This is what friendship looks like in 2025. ๐
A Next.js API that converts Resend newsletter broadcasts into an RSS feed with full HTML content. Built for desplega.ai to give power users RSS access to our newsletter without maintaining a separate distribution system.
๐ Update: Resend API Improvement
After posting about the limitations we encountered on Hacker News, Zeno Rocha (CEO of Resend) reached out and updated the Broadcast API to include HTML content directly. This allowed us to simplify the implementation by ~44% (262 โ 146 lines) and make it much faster. Thanks Resend team! ๐
Read about the initial implementation to see how user feedback can drive API improvements.
Why This Exists
At desplega.ai, we send newsletters via Resend. Some folks (looking at you, M) prefer RSS feeds over email. Instead of maintaining two separate systems, this API:
- โ Syncs broadcasts from Resend daily via cron
- โ Caches everything in Vercel Blob for fast access
- โ Serves a standard RSS feed with full HTML content
- โ Provides individual broadcast URLs for web viewing
Perfect for teams using Resend who want to offer RSS without the hassle.
Features
- Daily Auto-Sync: Cron job fetches new broadcasts from Resend every day
- Simple & Fast: Direct HTML from Broadcast API (~14s sync time)
- Full HTML Content: RSS feed includes complete email HTML (not just text)
- Individual Broadcast URLs: Each email is viewable at
/api/broadcast/{id} - Smart Caching: Uses Vercel Blob for fast RSS generation (no API calls)
- Rate-Limited: Respects Resend's 2 calls/second limit
- Type-Safe: Built with TypeScript
How It Works
โโโโโโโโโโโโโโโ
โ Resend API โ
โ Broadcasts โ โ Now includes HTML! ๐
โโโโโโโโฌโโโโโโโ
โ
โ Daily Cron (midnight UTC)
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. Fetch audiences โ
โ 2. Fetch broadcasts list โ
โ 3. Fetch each broadcast โ
โ (includes HTML + text) โ
โ 4. Convert HTML โ Markdown โ
โโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ Store in Vercel Blob
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ rss_audiences โ
โ rss_broadcasts โ
โ rss_broadcast_{id}_info โ โ Full broadcast w/ HTML
โ rss_broadcast_{id}_info_md โ โ Markdown version
โโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ
โ
โ Read from cache
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโ
โ /api/rss โ โ RSS Feed
โ /api/broadcast/{id} โ โ Individual broadcasts
โโโโโโโโโโโโโโโโโโโโโโโโ
Quick Start
Prerequisites
- Node.js 18+ and pnpm
- Resend API key
- Vercel account (for deployment)
Installation
git clone https://github.com/desplega-ai/rss.git
cd rss
pnpm installEnvironment Variables
Create a .env file:
# Required RESEND_API_KEY=re_xxxxx RESEND_AUDIENCE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx BLOB_READ_WRITE_TOKEN=vercel_blob_rw_xxxxx # Auto-generated by Vercel CRON_SECRET=your-random-secret-here # Optional: RSS Feed Customization RSS_FEED_TITLE="Newsletter Feed" RSS_FEED_DESCRIPTION="RSS feed of newsletter broadcasts"
Environment Variable Details:
| Variable | Required | Description | Default |
|---|---|---|---|
RESEND_API_KEY |
โ | Your Resend API key | - |
RESEND_AUDIENCE_ID |
โ | Target audience ID to fetch broadcasts for | - |
BLOB_READ_WRITE_TOKEN |
โ | Vercel Blob storage token (auto-generated) | - |
CRON_SECRET |
โ | Secret for authenticating cron requests | - |
RSS_FEED_TITLE |
โช | Custom title for your RSS feed | "Newsletter Feed" |
RSS_FEED_DESCRIPTION |
โช | Custom description for your RSS feed | "RSS feed of newsletter broadcasts" |
Local Development
# Start dev server pnpm dev # Trigger cron job manually (in another terminal) curl -X GET http://localhost:3000/api/cron \ -H "Authorization: Bearer your-cron-secret" # View RSS feed curl http://localhost:3000/api/rss # or open in browser open http://localhost:3000/api/rss
Build
Deployment to Vercel
-
Push to GitHub
-
Import to Vercel
- Go to vercel.com
- Click "New Project"
- Import your repository
-
Add Environment Variables
In Vercel project settings โ Environment Variables:
Required:
RESEND_API_KEY- Your Resend API keyRESEND_AUDIENCE_ID- Target audience IDCRON_SECRET- Random secret for cron authenticationBLOB_READ_WRITE_TOKEN- Auto-generated when you enable Vercel Blob
Optional (RSS Customization):
RSS_FEED_TITLE- Custom feed title (default: "Newsletter Feed")RSS_FEED_DESCRIPTION- Custom feed description (default: "RSS feed of newsletter broadcasts")
-
Enable Vercel Blob
- Go to Storage tab โ Create Blob Store
BLOB_READ_WRITE_TOKENwill be automatically set
-
Deploy
- Vercel will auto-deploy on push
- Cron runs daily at midnight UTC (configured in
vercel.json)
-
First Run (Manual Trigger)
curl -X GET https://your-app.vercel.app/api/cron \ -H "Authorization: Bearer your-cron-secret" -
Access Your RSS Feed
https://your-app.vercel.app/api/rss
API Endpoints
GET /api/rss
Returns RSS XML feed with all broadcasts.
Response:
<?xml version="1.0" encoding="UTF-8"?> <rss version="2.0"> <channel> <title>Newsletter Feed</title> <description>RSS feed of newsletter broadcasts</description> <item> <title>Newsletter Subject</title> <link>https://your-app.vercel.app/api/broadcast/{id}</link> <guid>https://your-app.vercel.app/api/broadcast/{id}</guid> <pubDate>Mon, 29 Sep 2025 18:54:10 GMT</pubDate> <description><![CDATA[Full HTML content here]]></description> </item> </channel> </rss>
GET /api/cron
Syncs data from Resend. Protected by Authorization header.
Headers:
Authorization: Bearer {CRON_SECRET}
Response:
{
"success": true,
"audiences": 1,
"broadcasts": 5
}Performance:
- Typical sync: ~14 seconds (fetches broadcast details with HTML)
GET /api/broadcast/[id]
Renders individual broadcast as HTML page.
Example:
https://your-app.vercel.app/api/broadcast/fa8f0216-3d02-4543-ad86-5e92e3c27124
History: From Limitation to Feature
The Original Problem
Initially, Resend's broadcast API didn't return email content (HTML/text). We had to build a complex workaround involving email fetching and matching. This worked but was slow (~4 minutes) and complex (~262 lines of code).
Read about the initial implementation โ
The Resolution
After we shared our experience on Hacker News, the Resend team (shoutout to Zeno!) updated the Broadcast API to include HTML content directly. This is a perfect example of how developer feedback can drive API improvements.
Impact:
- ๐ 44% code reduction (262 โ 146 lines)
- โก 94% faster (~14s vs 4 min)
- ๐ฏ No complex matching logic needed
- ๐งน Simpler architecture
Current Limitations
- Rate Limiting: Resend allows 2 API calls/second, so large broadcast lists may take time
- Vercel Blob: Free tier has storage limits; consider S3 for larger deployments (see Migration to S3)
Future Improvements
Migration to S3
Currently using Vercel Blob for storage. If you outgrow it or want more control:
Benefits of S3:
- Lower cost at scale
- More control over data
- Works outside Vercel ecosystem
- Better for multi-region deployments
Migration Path:
-
Install AWS SDK:
pnpm add @aws-sdk/client-s3
-
Replace
@vercel/blobimports with S3 client -
Update storage functions:
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'; const s3 = new S3Client({ region: 'us-east-1' }); // Instead of: put(key, data, options) await s3.send(new PutObjectCommand({ Bucket: 'your-bucket', Key: key, Body: data, }));
-
Update environment variables to use S3 credentials
The code structure is designed to make this swap easy - all storage operations are isolated in the cron route.
Development Notes
Rate Limiting
Resend allows 2 API calls per second. The cron job adds 500ms delays between calls to respect this limit.
Blob Storage Keys
rss_audiences # All audiences
rss_broadcasts # All broadcasts for target audience
rss_broadcast_{id}_info # Full broadcast with HTML
rss_broadcast_{id}_info_md # Markdown version (for future use)
rss_audience_{id} # Individual audience (for reference)
Dynamic RSS Rendering
The RSS endpoint uses export const dynamic = 'force-dynamic' to ensure it always fetches fresh data from Vercel Blob instead of serving a cached static version.
Tech Stack
- Next.js 14 - API routes
- TypeScript - Type safety
- Vercel Blob - Storage
- Vercel Cron - Scheduled jobs
- Turndown - HTML to Markdown (stored but not used in RSS)
- Resend API - Source data
Built by desplega.ai
This project was built by desplega.ai, a platform for building AI-powered browser automation and testing tools. We use this to provide RSS access to our newsletter while keeping everything automated through Resend.
If you're building AI agents that need to interact with web applications, check us out!
License
MIT License
Copyright (c) 2025 desplega.ai
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Made with โ by desplega.ai | Thanks M for the inspiration ๐