Skip to content

desplega-ai/rss

Repository files navigation

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 install

Environment 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

pnpm build

Deployment to Vercel

  1. Push to GitHub

    git push origin main
  2. Import to Vercel

    • Go to vercel.com
    • Click "New Project"
    • Import your repository
  3. Add Environment Variables

    In Vercel project settings → Environment Variables:

    Required:

    • RESEND_API_KEY - Your Resend API key
    • RESEND_AUDIENCE_ID - Target audience ID
    • CRON_SECRET - Random secret for cron authentication
    • BLOB_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")
  4. Enable Vercel Blob

    • Go to Storage tab → Create Blob Store
    • BLOB_READ_WRITE_TOKEN will be automatically set
  5. Deploy

    • Vercel will auto-deploy on push
    • Cron runs daily at midnight UTC (configured in vercel.json)
  6. First Run (Manual Trigger)

    curl -X GET https://your-app.vercel.app/api/cron \
      -H "Authorization: Bearer your-cron-secret"
  7. 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:

  1. Install AWS SDK:

    pnpm add @aws-sdk/client-s3
  2. Replace @vercel/blob imports with S3 client

  3. 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,
    }));
  4. 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 🎉