- JavaScript 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Syndication on rmendes.net has been blocked since the theme's OG slug change today: five posts sat at "not yet built (post: 200, og: 404)" while their cards were demonstrably live. The readiness gate derived the card's filename itself, via a helper whose docstring said "Matches Eleventy theme's ogSlug filter logic" — a hand-copied copy of another repo's convention. That convention changed (the theme added the post type to the cache key so a like and a reply sharing a basename stop colliding, theme 413f0cb), the copy did not, and the gate spent every poll HEADing a filename the theme no longer produces and the orphan prune had deleted. Silent: syndication just never fired. Now the gate GETs the post and HEADs whatever og:image the page declares. The page is the authority on its own card, so no convention is duplicated and any future slug change needs no coordination here. It also makes `skipOg` unnecessary in principle — a photo post declares its photo and that HEADs the same way — though the flag stays for callers that pass it. urlToOgSlug is deleted; the gate was its only consumer. Costs one GET instead of one HEAD, for posts awaiting syndication only. Verified against the live site: the post blocking Rick's delete test now reports ready=true post=200 og=200, as do an older post, a reply from the collision pair and a page with no generated card; a nonexistent URL correctly reports ready=false post=404. |
||
| .github/workflows | ||
| lib | ||
| test | ||
| .gitignore | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| index.js | ||
| package.json | ||
| README.md | ||
@rmdes/indiekit-endpoint-syndicate
Syndication endpoint for Indiekit. Fork of @indiekit/endpoint-syndicate with batch syndication support.
Features
- Single Post Syndication - Syndicate a specific post to configured targets
- Batch Syndication - Process all pending posts at once (useful for cron jobs)
- Webhook Support - Netlify webhook signature verification
- Failed Target Retry - Automatically retries failed syndication targets
- Rate Limiting - 2-second delay between posts in batch mode
- Detailed Results - Per-post success/failure tracking in batch mode
Differences from upstream
The Array.isArray and partial-syndication fixes this fork once carried are in upstream since 1.0.0-beta.29; the fork tracks upstream for them. What remains fork-only:
- Batch mode for processing all pending posts
- Detailed results array with success/failure per post, instead of failing the whole batch on one error
Installation
npm install @rmdes/indiekit-endpoint-syndicate
Configuration
Add to your indiekit.config.js:
import SyndicateEndpoint from "@rmdes/indiekit-endpoint-syndicate";
import BlueskyyndicatorSyndicator from "@rmdes/indiekit-syndicator-bluesky";
import MastodonSyndicator from "@rmdes/indiekit-syndicator-mastodon";
export default {
plugins: [
// Configure syndicators first
new BlueskyyndicatorSyndicator({
user: "username.bsky.social",
password: process.env.BLUESKY_PASSWORD
}),
new MastodonSyndicator({
url: "https://mastodon.social",
accessToken: process.env.MASTODON_TOKEN
}),
// Add syndication endpoint
new SyndicateEndpoint({
mountPath: "/syndicate" // Default: /syndicate
})
]
};
Environment Variables
# Required for token signing
SECRET=your-secret-key
# Required for Netlify webhook verification (optional)
WEBHOOK_SECRET=your-netlify-webhook-secret
Usage
Single Post Syndication
Query Parameters:
POST /syndicate?source_url=https://yoursite.com/posts/hello-world&token=YOUR_TOKEN
Request Body:
curl -X POST https://yoursite.com/syndicate \
-H "Content-Type: application/json" \
-d '{
"syndication": {
"source_url": "https://yoursite.com/posts/hello-world",
"redirect_uri": "/posts"
},
"access_token": "YOUR_TOKEN"
}'
Batch Syndication (All Pending Posts)
curl -X POST https://yoursite.com/syndicate?token=YOUR_TOKEN
This will:
- Find all posts with
mp-syndicate-toproperty - Process each post sequentially (2-second delay between posts)
- Return detailed results for each post
Netlify Webhook (Auto-Syndicate After Deploy)
Netlify Configuration:
- Go to Site Settings → Build & deploy → Deploy notifications
- Add notification: Outgoing webhook
- Event: Deploy succeeded
- URL:
https://yoursite.com/syndicate - Save webhook secret to
WEBHOOK_SECRETenv var
How It Works:
- Netlify deploys your site
- Netlify sends webhook with JWT signature
- Endpoint verifies signature, generates short-lived token
- Endpoint syndicates all pending posts
API Reference
POST /syndicate
Syndicate post(s) to external services.
Authentication:
- Bearer token (header, body, or query)
- OR Netlify webhook signature
Request:
Query parameters:
source_url(optional) - Specific post URL to syndicateredirect_uri(optional) - Redirect after success (must start with/)token(optional) - Bearer token
Body (JSON):
{
"syndication": {
"source_url": "https://yoursite.com/posts/hello-world",
"redirect_uri": "/posts"
},
"access_token": "YOUR_TOKEN"
}
Response (Single Post):
{
"success": "Post updated",
"success_description": "Added syndication https://bsky.app/profile/user/post/123"
}
Response (Batch Mode):
{
"success": "OK",
"success_description": "Processed 5 post(s): 4 succeeded, 1 failed",
"results": [
{
"url": "https://yoursite.com/posts/post-1",
"success": true,
"syndicatedUrls": [
"https://bsky.app/profile/user/post/123",
"https://mastodon.social/@user/456"
]
},
{
"url": "https://yoursite.com/posts/post-2",
"success": true,
"syndicatedUrls": ["https://bsky.app/profile/user/post/789"],
"failedTargets": ["https://mastodon.social/@user"]
},
{
"url": "https://yoursite.com/posts/post-3",
"success": false,
"error": "Micropub update failed: 401 Unauthorized"
}
]
}
How It Works
Data Flow
- Post Created - Micropub endpoint creates post with
mp-syndicate-toproperty - Trigger Received - Webhook, cron, or manual POST to
/syndicate - Query Posts - Find posts with
mp-syndicate-toin MongoDB - Syndicate - Call syndicator plugins for each target
- Update Post - Send Micropub update with syndicated URLs
- Cleanup - Remove
mp-syndicate-toif all targets succeeded
Post Properties
Before Syndication:
{
"url": "https://yoursite.com/posts/hello-world",
"mp-syndicate-to": [
"https://bsky.app/@username",
"https://mastodon.social/@username"
]
}
After Syndication (All Succeeded):
{
"url": "https://yoursite.com/posts/hello-world",
"syndication": [
"https://bsky.app/profile/username/post/abc123",
"https://mastodon.social/@username/456789"
]
}
After Syndication (One Failed):
{
"url": "https://yoursite.com/posts/hello-world",
"mp-syndicate-to": [
"https://mastodon.social/@username"
],
"syndication": [
"https://bsky.app/profile/username/post/abc123"
]
}
Syndicator Plugin Interface
Syndicator plugins must implement:
export default class MySyndicator {
get info() {
return {
uid: "https://example.com/@username",
name: "Example Service"
};
}
async syndicate(properties, publication) {
// Post to external service
// Return syndicated URL or null if failed
return "https://example.com/@username/post/123";
}
init(Indiekit) {
Indiekit.addSyndicator(this);
}
}
Common Use Cases
1. Cron Job (Daily Batch Syndication)
#!/bin/bash
# Crontab: 0 9 * * * /path/to/syndicate.sh
curl -X POST https://yoursite.com/syndicate?token=$INDIEKIT_TOKEN
2. GitHub Actions (Post-Deploy)
name: Deploy and Syndicate
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy site
run: # ... deploy steps ...
- name: Syndicate posts
run: |
curl -X POST https://yoursite.com/syndicate \
-H "Authorization: Bearer ${{ secrets.INDIEKIT_TOKEN }}"
3. Manual Trigger from Admin UI
<form method="POST" action="/syndicate">
<input type="hidden" name="syndication[source_url]" value="{{ post.url }}">
<input type="hidden" name="syndication[redirect_uri]" value="/posts">
<input type="hidden" name="access_token" value="{{ token }}">
<button type="submit">Syndicate Now</button>
</form>
Troubleshooting
Posts Not Being Syndicated
Check 1: Post has mp-syndicate-to property
db.posts.findOne({ "properties.url": "https://yoursite.com/posts/hello-world" })
// Should have: properties.mp-syndicate-to: ["https://bsky.app/@user", ...]
Check 2: Post is not a draft
// properties.post-status should NOT be "draft"
Check 3: Syndicators are configured
// In indiekit.config.js, syndicators must be loaded BEFORE syndicate endpoint
Check 4: Token is valid
# Test with explicit token
curl -X POST https://yoursite.com/syndicate?source_url=URL&token=TOKEN
Partial Syndication Not Retrying
This was a bug in upstream @indiekit/endpoint-syndicate. This fork fixes it.
Example:
- Post has
mp-syndicate-to: ["https://bsky.app", "https://mastodon.social"] - First run: Bluesky succeeds, Mastodon fails
- Post now has
mp-syndicate-to: ["https://mastodon.social"]andsyndication: ["https://bsky.app/..."] - Upstream: Post is skipped (has
syndicationproperty) - This fork: Post is processed, Mastodon is retried
Rate Limiting Errors
Batch mode waits 2 seconds between posts. If you still hit rate limits:
- Reduce batch size (manually split posts)
- Increase delay (modify
delay(2000)insyndicate.js) - Syndicate to fewer targets at once
Migration from Upstream
npm uninstall @indiekit/endpoint-syndicate
npm install @rmdes/indiekit-endpoint-syndicate
// Update import
import SyndicateEndpoint from "@rmdes/indiekit-endpoint-syndicate";
No other changes needed - fully backwards compatible.
Requirements
- Indiekit >= 1.0.0-beta.25
- MongoDB database (for posts collection)
- Syndicator plugins (e.g.,
@rmdes/indiekit-syndicator-bluesky) - Node.js >= 20
License
MIT
Author
Ricardo Mendes - https://rmendes.net
Repository
https://github.com/rmdes/indiekit-endpoint-syndicate
Upstream
Fork of @indiekit/endpoint-syndicate by Paul Lloyd.