@rmdes/indiekit-endpoint-syndicate 1.0.0-beta.44 → 1.0.0-beta.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/utils.js +78 -69
- package/package.json +2 -2
package/lib/utils.js
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Get post data for a single post
|
|
3
|
+
* Get every post awaiting syndication
|
|
4
|
+
*
|
|
5
|
+
* A post is awaiting syndication while `mp-syndicate-to` remains: it is
|
|
6
|
+
* deleted once every target has returned a URL, and replaced with the
|
|
7
|
+
* targets that failed otherwise. Posts already syndicated to some of their
|
|
8
|
+
* targets are therefore still awaiting the rest, and `syndicateToTargets`
|
|
9
|
+
* skips the ones already done. Oldest first, so a backlog is syndicated in
|
|
10
|
+
* the order it was published.
|
|
3
11
|
* @param {object} postsCollection - Posts database collection
|
|
4
|
-
* @
|
|
5
|
-
* @returns {Promise<object>} Post data for given URL else oldest post awaiting syndication
|
|
12
|
+
* @returns {Promise<Array<object>>} Post data, oldest first
|
|
6
13
|
*/
|
|
7
|
-
export const
|
|
8
|
-
|
|
9
|
-
return postsCollection.findOne({
|
|
10
|
-
"properties.url": url,
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// A post is awaiting syndication while `mp-syndicate-to` remains: it is
|
|
15
|
-
// deleted once every target has returned a URL, and replaced with the
|
|
16
|
-
// targets that failed otherwise. Posts already syndicated to some of their
|
|
17
|
-
// targets are therefore still awaiting the rest, and `syndicateToTargets`
|
|
18
|
-
// skips the ones already done. Oldest first, so a backlog is syndicated in
|
|
19
|
-
// the order it was published.
|
|
20
|
-
const items = await postsCollection
|
|
14
|
+
export const getPostsAwaitingSyndication = async (postsCollection) => {
|
|
15
|
+
return postsCollection
|
|
21
16
|
.find({
|
|
22
17
|
"properties.mp-syndicate-to": {
|
|
23
18
|
$exists: true,
|
|
@@ -27,10 +22,24 @@ export const getPostData = async (postsCollection, url) => {
|
|
|
27
22
|
},
|
|
28
23
|
})
|
|
29
24
|
.sort({ "properties.published": 1 })
|
|
30
|
-
.limit(1)
|
|
31
25
|
.toArray();
|
|
26
|
+
};
|
|
32
27
|
|
|
33
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Get post data
|
|
30
|
+
* @param {object} postsCollection - Posts database collection
|
|
31
|
+
* @param {string} url - URL of existing post (optional)
|
|
32
|
+
* @returns {Promise<object>} Post data for given URL else oldest post awaiting syndication
|
|
33
|
+
*/
|
|
34
|
+
export const getPostData = async (postsCollection, url) => {
|
|
35
|
+
if (url) {
|
|
36
|
+
return postsCollection.findOne({
|
|
37
|
+
"properties.url": url,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const [oldest] = await getPostsAwaitingSyndication(postsCollection);
|
|
42
|
+
return oldest;
|
|
34
43
|
};
|
|
35
44
|
|
|
36
45
|
/**
|
|
@@ -221,39 +230,6 @@ export const syndicateToTargets = async (
|
|
|
221
230
|
};
|
|
222
231
|
};
|
|
223
232
|
|
|
224
|
-
/**
|
|
225
|
-
* Derive OG image slug from a post URL path.
|
|
226
|
-
* Matches Eleventy theme's ogSlug filter logic.
|
|
227
|
-
*
|
|
228
|
-
* Date-based: /type/YYYY/MM/DD/slug → YYYY-MM-DD-slug
|
|
229
|
-
* Non-date: /about/ → about
|
|
230
|
-
* @param {string} postUrl - Full post URL
|
|
231
|
-
* @returns {string|null} OG slug or null
|
|
232
|
-
*/
|
|
233
|
-
export function urlToOgSlug(postUrl) {
|
|
234
|
-
try {
|
|
235
|
-
const pathname = new URL(postUrl).pathname;
|
|
236
|
-
const segments = pathname.split("/").filter(Boolean);
|
|
237
|
-
|
|
238
|
-
// Date-based URL: /type/yyyy/MM/dd/slug → 5 segments
|
|
239
|
-
if (segments.length === 5) {
|
|
240
|
-
const [, year, month, day, slug] = segments;
|
|
241
|
-
if (
|
|
242
|
-
/^\d{4}$/.test(year) &&
|
|
243
|
-
/^\d{2}$/.test(month) &&
|
|
244
|
-
/^\d{2}$/.test(day)
|
|
245
|
-
) {
|
|
246
|
-
return `${year}-${month}-${day}-${slug}`;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Fallback: last segment
|
|
251
|
-
return segments.at(-1);
|
|
252
|
-
} catch {
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
233
|
/**
|
|
258
234
|
* HEAD a URL and report its status, 0 on network error or timeout
|
|
259
235
|
* @param {string} url - URL to check
|
|
@@ -276,30 +252,63 @@ const headCheck = async (url) => {
|
|
|
276
252
|
};
|
|
277
253
|
|
|
278
254
|
/**
|
|
279
|
-
*
|
|
280
|
-
*
|
|
255
|
+
* GET a page and return its status plus the og:image it declares.
|
|
256
|
+
* @param {string} url - Page URL
|
|
257
|
+
* @returns {Promise<{status: number, ogImage?: string}>}
|
|
258
|
+
*/
|
|
259
|
+
const fetchOgImage = async (url) => {
|
|
260
|
+
try {
|
|
261
|
+
const controller = new AbortController();
|
|
262
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
263
|
+
const response = await fetch(url, {
|
|
264
|
+
redirect: "follow",
|
|
265
|
+
signal: controller.signal,
|
|
266
|
+
});
|
|
267
|
+
clearTimeout(timeoutId);
|
|
268
|
+
if (response.status !== 200) return { status: response.status };
|
|
269
|
+
const html = await response.text();
|
|
270
|
+
const match = html.match(/<meta\s+property="og:image"\s+content="([^"]+)"/);
|
|
271
|
+
return { status: 200, ogImage: match?.[1] };
|
|
272
|
+
} catch {
|
|
273
|
+
return { status: 0 };
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Check if a post and its OG card are live on the public site.
|
|
279
|
+
*
|
|
280
|
+
* Reads the og:image the POST ITSELF declares rather than re-deriving its
|
|
281
|
+
* filename. The removed helper's own docstring said "Matches Eleventy theme's
|
|
282
|
+
* ogSlug filter logic" — a hand-copied convention from another repo, and on
|
|
283
|
+
* 2026-09-13 that convention changed: the theme added the post type to the
|
|
284
|
+
* cache key so a like and a reply sharing a basename stop colliding. This gate
|
|
285
|
+
* kept asking for the old type-less name, got 404 for cards that were
|
|
286
|
+
* demonstrably live, and silently blocked syndication for every new post.
|
|
287
|
+
*
|
|
288
|
+
* Reading the page removes the coupling: whatever the page says its card is, we
|
|
289
|
+
* check that. It also makes `skipOg` unnecessary in principle — a photo post
|
|
290
|
+
* declares its photo as og:image and that HEADs the same way — though the flag
|
|
291
|
+
* is kept for callers that still pass it.
|
|
292
|
+
*
|
|
293
|
+
* Costs one GET instead of one HEAD, for posts awaiting syndication only.
|
|
281
294
|
* @param {string} postUrl - Full public URL of the post
|
|
282
|
-
* @param {string} me - Publication URL (
|
|
295
|
+
* @param {string} me - Publication URL (no longer used to build the card URL; kept for the signature)
|
|
283
296
|
* @param {object} [options] - Options
|
|
284
|
-
* @param {boolean} [options.skipOg] -
|
|
285
|
-
* where Eleventy uses the photo itself as og:image instead of generating /og/*.png)
|
|
297
|
+
* @param {boolean} [options.skipOg] - Don't wait on any og:image
|
|
286
298
|
* @returns {Promise<{ready: boolean, postStatus: number, ogStatus: number}>}
|
|
287
299
|
*/
|
|
288
300
|
export async function isPostReady(postUrl, me, options = {}) {
|
|
289
301
|
const { skipOg = false } = options;
|
|
290
|
-
const
|
|
291
|
-
const meNorm = me?.replace(/\/$/, "");
|
|
292
|
-
const ogUrl =
|
|
293
|
-
!skipOg && ogSlug && meNorm ? `${meNorm}/og/${ogSlug}.png` : undefined;
|
|
302
|
+
const { status: postStatus, ogImage } = await fetchOgImage(postUrl);
|
|
294
303
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
304
|
+
if (postStatus !== 200) {
|
|
305
|
+
return { ready: false, postStatus, ogStatus: 0 };
|
|
306
|
+
}
|
|
307
|
+
// No card declared (or explicitly skipped) → nothing to wait for.
|
|
308
|
+
if (skipOg || !ogImage) {
|
|
309
|
+
return { ready: true, postStatus, ogStatus: 200 };
|
|
310
|
+
}
|
|
299
311
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
postStatus,
|
|
303
|
-
ogStatus,
|
|
304
|
-
};
|
|
312
|
+
const ogStatus = await headCheck(ogImage);
|
|
313
|
+
return { ready: ogStatus === 200, postStatus, ogStatus };
|
|
305
314
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-syndicate",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.46",
|
|
4
4
|
"description": "Syndication endpoint for Indiekit. Fork of @indiekit/endpoint-syndicate with batch syndication support and bug fixes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"indiekit",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@indiekit/error": "^1.0.0-beta.28",
|
|
32
|
-
"express": "^5.
|
|
32
|
+
"express": "^5.3.0",
|
|
33
33
|
"jsonwebtoken": "^9.0.3"
|
|
34
34
|
},
|
|
35
35
|
"publishConfig": {
|