No description
  • JavaScript 100%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Ricardo 705c40c9ce fix: read the post's own og:image instead of re-deriving its filename
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.
2026-09-13 21:07:24 +02:00
.github/workflows chore: add CI and group Renovate updates 2026-09-04 08:59:13 +02:00
lib fix: read the post's own og:image instead of re-deriving its filename 2026-09-13 21:07:24 +02:00
test chore: typecheck clean under upstream's config 2026-09-13 14:45:07 +02:00
.gitignore fix: make batch syndication single-flight (v1.0.0-beta.39) 2026-09-02 23:41:16 +02:00
CHANGELOG.md v1.0.0-beta.29 2026-08-16 23:22:42 +00:00
CLAUDE.md fix: make batch syndication single-flight (v1.0.0-beta.39) 2026-09-02 23:41:16 +02:00
index.js style: prefer class fields 2025-08-02 16:36:55 +01:00
package.json fix: read the post's own og:image instead of re-deriving its filename 2026-09-13 21:07:24 +02:00
README.md docs: the Array.isArray and partial-syndication fixes are upstream since beta.29 2026-09-08 13:45:35 +02:00

@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:

  1. Find all posts with mp-syndicate-to property
  2. Process each post sequentially (2-second delay between posts)
  3. Return detailed results for each post

Netlify Webhook (Auto-Syndicate After Deploy)

Netlify Configuration:

  1. Go to Site Settings → Build & deploy → Deploy notifications
  2. Add notification: Outgoing webhook
  3. Event: Deploy succeeded
  4. URL: https://yoursite.com/syndicate
  5. Save webhook secret to WEBHOOK_SECRET env var

How It Works:

  1. Netlify deploys your site
  2. Netlify sends webhook with JWT signature
  3. Endpoint verifies signature, generates short-lived token
  4. 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 syndicate
  • redirect_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

  1. Post Created - Micropub endpoint creates post with mp-syndicate-to property
  2. Trigger Received - Webhook, cron, or manual POST to /syndicate
  3. Query Posts - Find posts with mp-syndicate-to in MongoDB
  4. Syndicate - Call syndicator plugins for each target
  5. Update Post - Send Micropub update with syndicated URLs
  6. Cleanup - Remove mp-syndicate-to if 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"] and syndication: ["https://bsky.app/..."]
  • Upstream: Post is skipped (has syndication property)
  • 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) in syndicate.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.