Automatically syndicate your RSS feed to Bluesky and Mastodon. Runs on Cloudflare Workers as a scheduled cron job.
Dev.to: has native RSS import built in. Go to dev.to/settings/extensions to connect your feed directly. Posts land as drafts for review before publishing.
- Polls any RSS 2.0 feed on a configurable cron schedule
- Posts to Bluesky with a rich link card (OG title, description, thumbnail via AT Protocol embed)
- Posts to Mastodon with title, excerpt, and link
- Deduplicates via Cloudflare KV — each post syndicates exactly once
- Auto-bootstraps on first run: records a sync floor so existing posts are never published
- Adapters are optional — configure only the platforms you want
- Cloudflare account (free tier sufficient)
- Wrangler CLI installed and authenticated (
wrangler login) - Node.js 22+
git clone https://github.com/devenney/rss-to-social.git
cd rss-to-social
npm installnpm run setupCreates both KV namespaces and generates wrangler.personal.toml with the IDs filled in. This file is gitignored and never committed.
[vars]
RSS_FEED_URL = "https://your-site.com/rss.xml"
BLUESKY_HANDLE = "you.bsky.social"
MASTODON_INSTANCE = "mastodon.social"- Bluesky: bsky.app/settings/app-passwords → Add App Password
- Mastodon:
https://[instance]/settings/applications→ New Application, tickwrite:statuses
npm run secretsPrompts for BLUESKY_APP_PASSWORD then MASTODON_TOKEN. Secrets are stored in Cloudflare and never appear in any file.
npm run deployThe worker auto-bootstraps on its first cron tick.
On first run the worker records the current timestamp as a sync floor. Posts published before that time are permanently skipped — protecting against back-catalogue flooding on initial deploy.
Seen post GUIDs are tracked in KV to prevent duplicates after the floor.
If the KV namespace is ever wiped, the sync floor resets on the next run. To backfill from a specific date:
npm run bootstrap -- --from=2026-01-01npm run nudge -- --url=https://your-site.com/blog/the-missed-postRemoves the post's GUID from KV. The next cron tick re-syndicates it.
CI (lint → typecheck → tests) runs on every push and pull request. Deployment and releases trigger automatically when CI passes on main.
Add these secrets to your GitHub repository (Settings → Secrets and variables → Actions):
| Secret | How to get it |
|---|---|
CLOUDFLARE_API_TOKEN |
Cloudflare dashboard → My Profile → API Tokens → "Edit Cloudflare Workers" template |
CLOUDFLARE_ACCOUNT_ID |
Cloudflare dashboard → any Workers page → right sidebar |
KV_NAMESPACE_ID |
Production KV namespace ID (from npm run setup) |
RSS_FEED_URL |
Your feed URL |
BLUESKY_HANDLE |
Your Bluesky handle |
MASTODON_INSTANCE |
Your Mastodon instance hostname |
Releases are tagged automatically by semantic-release from conventional commits.
Edit wrangler.personal.toml:
[triggers]
crons = ["0 * * * *"] # hourly; adjust to taste- Implement
SocialAdapterfromsrc/ports/social.ts - Add tests in
test/adapters/ - Register in
src/config.ts
npm run dev # wrangler dev with wrangler.personal.toml
npm test # tests run in Workers runtime, no network calls
npm run lint
npm run typecheckMIT