Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions packages/endpoint-micropub/lib/post-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
77 changes: 76 additions & 1 deletion packages/endpoint-micropub/test/unit/post-content.js
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
});
});
4 changes: 4 additions & 0 deletions packages/endpoint-syndicate/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
20 changes: 20 additions & 0 deletions packages/endpoint-syndicate/test/unit/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getPostData,
getSyndicationTarget,
hasSyndicationUrl,
syndicateToTargets,
} from "../../lib/utils.js";

const { client, database, mongoServer } = await testDatabase();
Expand Down Expand Up @@ -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);
});
});