From 5b953eee34d360b3b67d0dbb661df2b41fe0ae8e Mon Sep 17 00:00:00 2001 From: Anthony Ciccarello Date: Sun, 16 Aug 2026 16:44:33 -0700 Subject: [PATCH 1/4] feat(endpoint-syndicate): make syndicate() optional on syndicator targets --- packages/endpoint-syndicate/lib/utils.js | 4 ++++ .../endpoint-syndicate/test/unit/utils.js | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/endpoint-syndicate/lib/utils.js b/packages/endpoint-syndicate/lib/utils.js index a8d8e8c0c..580317635 100644 --- a/packages/endpoint-syndicate/lib/utils.js +++ b/packages/endpoint-syndicate/lib/utils.js @@ -79,6 +79,10 @@ export const syndicateToTargets = async (publication, properties) => { const alreadySyndicated = hasSyndicationUrl(syndicatedUrls, url); if (target && !alreadySyndicated) { + if (typeof target.syndicate !== "function") { + continue; + } + try { const syndicatedUrl = await target.syndicate(properties, publication); diff --git a/packages/endpoint-syndicate/test/unit/utils.js b/packages/endpoint-syndicate/test/unit/utils.js index 1ff53f61e..49c0f1b6b 100644 --- a/packages/endpoint-syndicate/test/unit/utils.js +++ b/packages/endpoint-syndicate/test/unit/utils.js @@ -7,6 +7,7 @@ import { getPostData, getSyndicationTarget, hasSyndicationUrl, + syndicateToTargets, } from "../../lib/utils.js"; const { client, database, mongoServer } = await testDatabase(); @@ -87,4 +88,23 @@ describe("endpoint-syndicate/lib/token", () => { false, ); }); + + it("Skips syndicator target without syndicate method", async () => { + const targetWithoutSyndicate = { + info: { uid: "https://example.com/" }, + // no syndicate method + }; + const publication = { + syndicationTargets: [targetWithoutSyndicate], + }; + const properties = { + "mp-syndicate-to": ["https://example.com/"], + url: "https://me.example/post/1", + }; + + const result = await syndicateToTargets(publication, properties); + + assert.deepEqual(result.syndicatedUrls, []); + assert.equal(result.failedTargets, undefined); + }); }); From 441c74a43f27fbf277edfb24cc659336d9e4c7b9 Mon Sep 17 00:00:00 2001 From: Anthony Ciccarello Date: Sun, 16 Aug 2026 16:52:40 -0700 Subject: [PATCH 2/4] feat(endpoint-micropub): call getSyndicationUrl hook on syndicators at post creation --- .../endpoint-micropub/lib/post-content.js | 24 ++++++++ .../test/unit/post-content.js | 59 ++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/packages/endpoint-micropub/lib/post-content.js b/packages/endpoint-micropub/lib/post-content.js index 78f6c205b..f9da3b2e7 100644 --- a/packages/endpoint-micropub/lib/post-content.js +++ b/packages/endpoint-micropub/lib/post-content.js @@ -16,6 +16,30 @@ export const postContent = { const { postTemplate, store, storeMessageTemplate } = publication; const { path, properties } = postData; + + // Call getSyndicationUrl on any targeted syndicator that implements it + const syndicationTargets = publication.syndicationTargets ?? []; + const requestedUids = [properties["mp-syndicate-to"] ?? []].flat(); + for (const target of syndicationTargets) { + if ( + typeof target.getSyndicationUrl === "function" && + requestedUids.includes(target.info.uid) + ) { + try { + const urls = [await target.getSyndicationUrl(publication)] + .flat() + .filter(Boolean); + const existing = [properties.syndication ?? []].flat(); + const newUrls = urls.filter((url) => !existing.includes(url)); + if (newUrls.length > 0) { + properties.syndication = [...existing, ...newUrls]; + } + } catch (error) { + debug(`getSyndicationUrl failed for ${target.info.uid}: %O`, error); + } + } + } + const metadata = { action: "create", result: "created", diff --git a/packages/endpoint-micropub/test/unit/post-content.js b/packages/endpoint-micropub/test/unit/post-content.js index f4d810169..41bca404d 100644 --- a/packages/endpoint-micropub/test/unit/post-content.js +++ b/packages/endpoint-micropub/test/unit/post-content.js @@ -1,5 +1,5 @@ import { strict as assert } from "node:assert"; -import { describe, it } from "node:test"; +import { describe, it, mock } from "node:test"; import { mockAgent } from "@indiekit-test/mock-agent"; import { deletedPostData, postData } from "@indiekit-test/post-data"; @@ -86,4 +86,61 @@ describe("endpoint-micropub/lib/post-content", () => { message: "postTemplate is not a function", }); }); + + it("Calls getSyndicationUrl on targeted syndicator and sets syndication property", async () => { + const getSyndicationUrl = mock.fn(async () => "https://news.indieweb.org/en/"); + const syndicator = { + info: { uid: "https://news.indieweb.org/en/" }, + getSyndicationUrl, + }; + const pub = { ...publication, syndicationTargets: [syndicator] }; + const data = { + path: "foo.md", + properties: { + ...postData.properties, + "mp-syndicate-to": ["https://news.indieweb.org/en/"], + }, + }; + await postContent.create(pub, data); + assert.equal(getSyndicationUrl.mock.calls.length, 1); + }); + + it("Does not call getSyndicationUrl on syndicator not in mp-syndicate-to", async () => { + const getSyndicationUrl = mock.fn(async () => "https://news.indieweb.org/en/"); + const syndicator = { + info: { uid: "https://news.indieweb.org/en/" }, + getSyndicationUrl, + }; + const pub = { ...publication, syndicationTargets: [syndicator] }; + const data = { + path: "foo.md", + properties: { + ...postData.properties, + "mp-syndicate-to": ["https://other.example/"], + }, + }; + await postContent.create(pub, data); + assert.equal(getSyndicationUrl.mock.calls.length, 0); + }); + + it("Appends to existing syndication without duplicates", async () => { + const getSyndicationUrl = mock.fn(async () => "https://news.indieweb.org/en/"); + const syndicator = { + info: { uid: "https://news.indieweb.org/en/" }, + getSyndicationUrl, + }; + const pub = { ...publication, syndicationTargets: [syndicator] }; + const data = { + path: "foo.md", + properties: { + ...postData.properties, + syndication: ["https://news.indieweb.org/en/"], + "mp-syndicate-to": ["https://news.indieweb.org/en/"], + }, + }; + await postContent.create(pub, data); + // getSyndicationUrl was called but returned a duplicate — syndication stays length 1 + assert.equal(getSyndicationUrl.mock.calls.length, 1); + assert.deepEqual(data.properties.syndication, ["https://news.indieweb.org/en/"]); + }); }); From baf55f486171526b7b1b08268a499623517b6763 Mon Sep 17 00:00:00 2001 From: Anthony Ciccarello Date: Sun, 16 Aug 2026 16:54:32 -0700 Subject: [PATCH 3/4] test(endpoint-micropub): assert syndication property set in post-content test --- packages/endpoint-micropub/test/unit/post-content.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/endpoint-micropub/test/unit/post-content.js b/packages/endpoint-micropub/test/unit/post-content.js index 41bca404d..f305fe528 100644 --- a/packages/endpoint-micropub/test/unit/post-content.js +++ b/packages/endpoint-micropub/test/unit/post-content.js @@ -103,6 +103,7 @@ describe("endpoint-micropub/lib/post-content", () => { }; await postContent.create(pub, data); assert.equal(getSyndicationUrl.mock.calls.length, 1); + assert.deepEqual(data.properties.syndication, ["https://news.indieweb.org/en/"]); }); it("Does not call getSyndicationUrl on syndicator not in mp-syndicate-to", async () => { From 8d6bac7136c3486366385cd9664c2f081570b36e Mon Sep 17 00:00:00 2001 From: Anthony Ciccarello Date: Sun, 16 Aug 2026 17:01:13 -0700 Subject: [PATCH 4/4] test: add error path coverage and verify failedTargets assertion --- .../endpoint-micropub/test/unit/post-content.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/endpoint-micropub/test/unit/post-content.js b/packages/endpoint-micropub/test/unit/post-content.js index f305fe528..d7592ae03 100644 --- a/packages/endpoint-micropub/test/unit/post-content.js +++ b/packages/endpoint-micropub/test/unit/post-content.js @@ -144,4 +144,21 @@ describe("endpoint-micropub/lib/post-content", () => { assert.equal(getSyndicationUrl.mock.calls.length, 1); assert.deepEqual(data.properties.syndication, ["https://news.indieweb.org/en/"]); }); + + it("Silently skips syndicator when getSyndicationUrl throws", async () => { + const syndicator = { + info: { uid: "https://news.indieweb.org/en/" }, + getSyndicationUrl: async () => { throw new Error("network error"); }, + }; + const pub = { ...publication, syndicationTargets: [syndicator] }; + const data = { + path: "foo.md", + properties: { + ...postData.properties, + "mp-syndicate-to": ["https://news.indieweb.org/en/"], + }, + }; + await assert.doesNotReject(() => postContent.create(pub, data)); + assert.equal(data.properties.syndication, undefined); + }); });