A Cloudflare Worker that automatically syncs bookmarks from Raindrop.io to WordPress as link posts. Tag a bookmark with your configured tag in Raindrop, and it will automatically appear as a WordPress post with proper formatting and metadata.
- 🔄 Automatic Sync: Hourly cron job syncs new bookmarks
- 🏷️ Tag-based Filtering: Only syncs bookmarks with specific tags
- 📝 Markdown Support: Converts Raindrop notes to WordPress content
- 🔗 Link Post Format: Creates proper WordPress link posts
- 🚫 Deduplication: Prevents duplicate posts
- 📊 Error Logging: Built-in error tracking and reporting
- 🧪 Dry Run Mode: Test without creating actual posts
- ⚡ Edge Computing: Fast, globally distributed via Cloudflare Workers
- Cloudflare account (free tier works)
- Raindrop.io account
- WordPress site with REST API enabled
- Node.js 18+ and npm
git clone https://github.com/rianvdm/raindrop-wordpress-linkblog-sync.git
cd raindrop-wordpress-linkblog-sync
npm install- Go to Raindrop.io App Settings
- Create a new Test Token (not OAuth app)
- Copy the token - you'll need it for
RAINDROP_TOKEN - Choose a tag name (e.g., "blog") for bookmarks you want to sync
- Ensure your WordPress site has the REST API enabled (it is by default)
- Create a user account for the sync service or use existing account
- Generate an Application Password:
- Go to WordPress Admin → Users → Your Profile
- Scroll to "Application Passwords"
- Create new password with name like "Raindrop Sync"
- Copy the generated password (you won't see it again)
- Verify your site supports the "link" post format:
- Go to Appearance → Theme Features → Post Formats
- Enable "Link" format if not already enabled
Create a .dev.vars file for local development:
# .dev.vars (for local development only)
RAINDROP_TOKEN=your_raindrop_token_here
WP_USERNAME=your_wordpress_username
WP_APP_PASSWORD=your_wordpress_app_password
WP_ENDPOINT=https://yoursite.com/wp-json/wp/v2/posts
TRIGGER_TOKEN=a_secure_random_string_for_manual_triggersUpdate wrangler.toml:
name = "your-sync-worker-name"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[[kv_namespaces]]
binding = "SYNC_STATE"
id = "your_kv_namespace_id"
preview_id = "your_preview_kv_namespace_id"
[[kv_namespaces]]
binding = "RAINDROP_ERRORS"
id = "your_error_kv_namespace_id"
preview_id = "your_error_preview_kv_namespace_id"
[vars]
RAINDROP_TAG = "your_chosen_tag" # e.g., "blog"
[triggers]
crons = ["0 * * * *"] # Run every hour at minute 0# Create production KV namespaces
npx wrangler kv:namespace create "SYNC_STATE"
npx wrangler kv:namespace create "RAINDROP_ERRORS"
# Create preview KV namespaces
npx wrangler kv:namespace create "SYNC_STATE" --preview
npx wrangler kv:namespace create "RAINDROP_ERRORS" --preview
# Update the IDs in wrangler.toml with the returned namespace IDs# Deploy the worker
npm run deploy
# Set production secrets (replace with your actual values)
npx wrangler secret put RAINDROP_TOKEN
npx wrangler secret put WP_USERNAME
npx wrangler secret put WP_APP_PASSWORD
npx wrangler secret put WP_ENDPOINT
npx wrangler secret put TRIGGER_TOKEN# Test locally first
npm run dev
# In another terminal, test the trigger endpoint
curl "http://localhost:8787/trigger?token=your_trigger_token&dry_run=true"
# Test in production (replace with your worker URL)
curl "https://your-worker.your-subdomain.workers.dev/trigger?token=your_trigger_token&dry_run=true"When a bookmark is synced, it creates a WordPress post with the "link" format using:
- Post Title: Uses the bookmark's title from Raindrop.io
- Post Content:
- Converts the bookmark's note (if any) from Markdown to HTML
- Adds a formatted link at the bottom:
→ [Bookmark Title](URL) - Example output:
<p>This is my note about the article, with <strong>markdown</strong> support.</p> <p>Source: <a href="https://example.com" target="_blank" rel="noopener">Article Title</a> ↗</p>
- Post Status: Published immediately (not draft)
- Post Format: Set to "link" for proper theme support
- Deduplication: Prevents creating duplicate posts for the same bookmark
The worker runs automatically every hour. Tag any bookmark in Raindrop.io with your configured tag and it will be synced to WordPress on the next run.
Trigger a sync manually via the API:
# Basic sync
curl "https://your-worker.workers.dev/trigger?token=your_trigger_token"
# Dry run (test without creating posts)
curl "https://your-worker.workers.dev/trigger?token=your_trigger_token&dry_run=true"
# Sync specific tag
curl "https://your-worker.workers.dev/trigger?token=your_trigger_token&tag=different_tag"
# Limit number of items
curl "https://your-worker.workers.dev/trigger?token=your_trigger_token&limit=5"If you want to sync older bookmarks:
# Reset to sync bookmarks from last 90 days
curl "https://your-worker.workers.dev/reset-timestamp?token=your_trigger_token&days=90"Check sync errors and logs:
curl "https://your-worker.workers.dev/errors?token=your_trigger_token"Clear all error logs from storage:
curl -X POST "https://your-worker.workers.dev/clear-errors?token=your_trigger_token"| Endpoint | Method | Parameters | Description |
|---|---|---|---|
/trigger |
GET | token, dry_run, tag, limit |
Trigger manual sync |
/reset-timestamp |
POST | token, days |
Reset last sync timestamp |
/errors |
GET | token |
View error logs |
/clear-errors |
POST | token |
Clear all error logs |
| Variable | Required | Description |
|---|---|---|
RAINDROP_TOKEN |
✅ | Raindrop.io API token |
WP_USERNAME |
✅ | WordPress username |
WP_APP_PASSWORD |
✅ | WordPress application password |
WP_ENDPOINT |
✅ | WordPress REST API endpoint |
TRIGGER_TOKEN |
✅ | Secure token for API access |
RAINDROP_TAG |
✅ | Tag to filter bookmarks (set in wrangler.toml) |
Default: "0 * * * *" (every hour at minute 0)
Common alternatives:
"0 */6 * * *"- Every 6 hours"0 9,17 * * *"- Twice daily at 9 AM and 5 PM"30 * * * *"- Every hour at 30 minutes past
# Start local development server
npm run dev
# Run tests
npm test
# Run linting
npm run lint
# Type checking
npm run type-checksrc/
├── index.ts # Main worker entry point
├── router.ts # Request routing
├── middleware/ # Authentication middleware
├── services/ # Core business logic
│ ├── raindrop-client.ts # Raindrop API client
│ ├── wordpress-client.ts # WordPress API client
│ ├── sync-orchestrator.ts # Main sync logic
│ ├── kv-storage.ts # KV storage operations
│ ├── error-logger.ts # Error logging
│ ├── markdown-processor.ts # Markdown conversion
│ └── content-builder.ts # WordPress content formatting
├── utils/ # Utility functions
└── types/ # TypeScript type definitions
- Create feature branch:
git checkout -b feature/your-feature - Implement changes with tests
- Run test suite:
npm test - Run linting:
npm run lint - Commit and push changes
- Create pull request
The project includes automated CI/CD:
-
Required Secrets in GitHub repository settings:
CLOUDFLARE_API_TOKEN: Cloudflare API token with Workers edit permissionsCLOUDFLARE_ACCOUNT_ID: Your Cloudflare account ID
-
Automatic Deployment: Pushes to
mainbranch automatically deploy to production -
Pipeline Steps:
- Run tests
- Type checking
- Linting
- Deploy to Cloudflare Workers
"Authentication failed" errors:
- Verify WordPress username and application password
- Check that WP_ENDPOINT URL is correct
- Ensure WordPress REST API is enabled
"No bookmarks found" but bookmarks exist:
- Verify RAINDROP_TAG matches your bookmark tags exactly
- Check that RAINDROP_TOKEN has correct permissions
- Try resetting the sync timestamp with
/reset-timestamp
Worker deployment fails:
- Verify Cloudflare account ID and API token
- Check that KV namespace IDs in wrangler.toml are correct
- Ensure all required secrets are set
Old bookmarks not syncing:
- The sync only processes bookmarks modified after the last sync
- Use
/reset-timestamp?days=90to sync older bookmarks - Tag modification in Raindrop updates the
lastUpdatefield
View worker logs in Cloudflare Dashboard:
- Go to Workers & Pages
- Click your worker name
- View "Logs" tab for real-time debugging
Check error logs via API:
curl "https://your-worker.workers.dev/errors?token=your_trigger_token"- All API endpoints require token authentication
- Secrets are stored securely in Cloudflare Workers
- CORS headers configured for browser access
- No sensitive data logged or exposed
MIT License - see LICENSE file for details.
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Submit a pull request
For issues and questions:
- Check the troubleshooting section
- Search existing issues
- Create a new issue with detailed description
Happy syncing! 🔄📝