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..d7592ae03 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,79 @@ 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); + assert.deepEqual(data.properties.syndication, ["https://news.indieweb.org/en/"]); + }); + + 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/"]); + }); + + 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); + }); }); 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); + }); });