Skip to content

Syndication slice 1: Syndicator + Dev.to adapter + storage + capability - #1

Merged
DanMat merged 1 commit into
mainfrom
feat/syndication-slice-1
Sep 6, 2026
Merged

DanMat merged 1 commit into
mainfrom
feat/syndication-slice-1

Conversation

@DanMat

@DanMat DanMat commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

The engine for cross-posting a blog post to external dev platforms (Dev.to first), driven from both the admin and MCP once slice 2 wires the surfaces. This slice is the shared core, unit-tested with a faked HTTP client, no user-facing surface yet.

What

  • Syndicator — reads a published post (published-only content reader), computes the canonical (the post's own canonical_url if it's a copy, else the self /blog/{slug} URL), calls a target adapter, and records the result so a repeat push updates, not duplicates. The one method both surfaces will call.
  • SyndicationTarget adapter interface + DevToTarget (Forem: POST create / PUT update by id, api-key header, canonical + tags sanitised to Dev.to's rules; non-2xx → SyndicationError). New targets are new adapters.
  • HttpClient seam (CurlHttpClient default, faked in tests) so nothing touches the network in tests.
  • SyndicationRepository over the plugin's first table blog_syndication (entry_id + target → external id/url, upsert = update-not-duplicate), behind a SyndicationStore interface so the service is DB-free testable.
  • New capability nimbuscms.blog:syndicate (wildcard-immune, fine-grained) declared; migration 001_syndication registered. Secrets (DEVTO_API_KEY) read server-side, never in the DB/MCP/logs.

Reviews

Design + platform + security review in docs/DESIGN-syndication.md. Fixed outbound hosts (no SSRF), secrets server-side, capability opt-in on both surfaces, idempotent by external id.

Tests

DevToTargetTest (create/update request shape, tags, non-2xx), SyndicatorTest (canonical self vs declared, create vs update, unknown target, missing post, failure records error), SyndicationRepositoryTest (insert-then-update-in-place, per-target listing). 27 tests green, PHPStan L6 + cs-fixer clean.

Next: slice 2 wires the Syndication admin page + the syndicate_post / syndication_status MCP tools onto this, both gated blog:syndicate.

🤖 Generated with Claude Code

…capability

The shared core for cross-posting a blog post to external dev platforms, from both
the admin and MCP later (slice 2). This slice is the engine, unit-tested with a
faked HTTP client, no user-facing surface yet.

- Syndicator: reads a published post (published-only content reader), computes the
  canonical (the post's canonical_url if it is itself a copy, else the self /blog
  URL), calls a target adapter, and records the result so a repeat push updates
  instead of duplicating. One method both surfaces will call.
- SyndicationTarget adapter interface + DevToTarget (Forem API: POST create /
  PUT update by id, api-key header, canonical + sanitised tags [lowercase
  alphanumeric, max 4]); a non-2xx becomes a clear SyndicationError.
- HttpClient seam (CurlHttpClient default, faked in tests) so no network in tests.
- SyndicationStore interface + SyndicationRepository over the plugin's first table
  blog_syndication (entry_id+target -> external id/url, upsert = update-not-duplicate).
- New capability nimbuscms.blog:syndicate (wildcard-immune, fine-grained) declared;
  migration 001_syndication registered. Secrets (DEVTO_API_KEY) read server-side.

Design + reviews in docs/DESIGN-syndication.md. 27 tests green, PHPStan L6 + cs-fixer
clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@DanMat
DanMat merged commit b8258cd into main Sep 6, 2026
2 checks passed
@DanMat
DanMat deleted the feat/syndication-slice-1 branch September 6, 2026 23:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant