A lightweight Node.js bot that monitors RSS feeds and posts new articles to Bluesky with rich embed cards.
- Monitors multiple RSS feeds on a configurable polling interval
- Posts new articles with Open Graph metadata (title, description, thumbnail)
- Tracks posted links locally to prevent duplicates
- Persistent session management (logs in once, re-authenticates on expiry)
- Respects Bluesky API rate limits with separate read/write tracking
- Request timeouts and URL validation for reliability and security
- Runs as non-root user in Docker with health checks
git clone https://github.com/cgillinger/Blueskybot.git
cd Blueskybot
npm installcp .env.example .envEdit .env with your Bluesky credentials:
BLUESKY_USERNAME=your_handle@bsky.social
BLUESKY_PASSWORD=your_app_passwordTip: Use an App Password instead of your main password.
cp feeds.txt.example feeds.txtEdit feeds.txt — one feed per line, no quotes or brackets needed:
https://example.com/feed.xml | Example News
https://another.site/rss | Another Feed
https://minimal.org/rss
Lines starting with # are comments. The title after | is optional — if provided, it prefixes the Bluesky post.
npm startThe bot polls every minute and posts articles published within the last hour. Conditional HTTP requests (ETag/Last-Modified) keep unchanged polls near-zero cost.
cp .env.example .env # configure credentials
cp feeds.txt.example feeds.txt # configure feeds
docker compose up -d --builddocker compose logs -f # follow logs
docker compose down # stopdocker build -t blueskybot .
docker run -d --name blueskybot --env-file .env --restart always blueskybotThe container uses node:18-alpine, runs as a non-root user, and includes a health check.
All configuration constants are defined at the top of bot.mjs:
| Constant | Default | Description |
|---|---|---|
POLL_INTERVAL_MS |
60000 |
Polling interval (1 min) |
PUBLICATION_WINDOW_MS |
3600000 |
Only post articles newer than this (1 hour) |
MAX_TRACKED_LINKS_PER_FEED |
20 |
Duplicate tracking buffer per feed |
FETCH_TIMEOUT_MS |
15000 |
HTTP request timeout (15 sec) |
MAX_IMAGE_SIZE |
1000000 |
Max thumbnail size in bytes (1 MB) |
Blueskybot/
├── bot.mjs # Main application
├── feeds.txt # Your RSS feeds (not tracked by git)
├── feeds.txt.example # Feed configuration template
├── Dockerfile # Container image (Alpine, non-root)
├── docker-compose.yml # Compose orchestration
├── package.json # Dependencies and scripts
├── .env.example # Credential template
├── .gitignore
├── LICENSE # MIT
└── README.md
┌─────────────┐ ┌──────────────┐ ┌─────────────────┐
│ RSS Feeds │────>│ bot.mjs │────>│ Bluesky (AT │
│ (polling) │ │ parse/filter│ │ Protocol API) │
└─────────────┘ └──────┬───────┘ └─────────────────┘
│
┌──────┴───────┐
│ OG metadata │
│ fetch + image│
│ upload │
└──────┬───────┘
│
┌──────┴───────┐
│ lastPosted │
│ Links.json │
└──────────────┘
- Poll RSS feeds at a fixed interval
- Filter articles to those published within the last hour
- Deduplicate against locally stored posted links
- Fetch Open Graph metadata (title, description, image) from article URL
- Upload thumbnail image as blob to Bluesky
- Post to Bluesky with
app.bsky.embed.externalembed card - Persist the posted link to avoid duplicates on restart
| Problem | Solution |
|---|---|
Invalid identifier or password |
Verify .env credentials. Use an App Password. |
API rate limit reached |
The bot automatically waits and retries. No action needed. |
| Thumbnails missing on some posts | The source site may lack og:image tags, or the image exceeds 1 MB. |
FETCH_TIMEOUT errors |
The target site is slow or unreachable. The post will still be created without a thumbnail. |
| Container unhealthy | Check logs with docker compose logs — likely a credential or network issue. |
Contributions are welcome! Please open an issue or submit a pull request.
MIT © Christian Gillinger