@rmdes/indiekit-endpoint-syndicate 1.0.0-beta.37 → 1.0.0-beta.39
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/controllers/syndicate.js +98 -64
- package/lib/utils.js +11 -3
- package/package.json +4 -1
|
@@ -15,6 +15,24 @@ import {
|
|
|
15
15
|
*/
|
|
16
16
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Guards batch mode against overlapping runs.
|
|
20
|
+
*
|
|
21
|
+
* A post stays selectable until its Micropub update lands at the end of a
|
|
22
|
+
* batch, so a second trigger arriving mid-batch sees `mp-syndicate-to` still
|
|
23
|
+
* set and syndicates it again. Both triggers — the poller and the Eleventy
|
|
24
|
+
* post-build hook — reach the same process, so a module-level flag is enough.
|
|
25
|
+
* Anything skipped is picked up by the next poll.
|
|
26
|
+
*
|
|
27
|
+
* Batch mode only: the single-post force path (source_url) is deliberately
|
|
28
|
+
* unguarded so manual re-syndication always runs.
|
|
29
|
+
*
|
|
30
|
+
* ponytail: single-process guard. If Indiekit is ever run multi-process or
|
|
31
|
+
* multi-container, replace with an atomic per-post claim (findOneAndUpdate
|
|
32
|
+
* setting a claim timestamp, with a stale-claim timeout).
|
|
33
|
+
*/
|
|
34
|
+
let batchRunning = false;
|
|
35
|
+
|
|
18
36
|
/**
|
|
19
37
|
* Syndicate a single post and update it via Micropub
|
|
20
38
|
* @param {object} options - Options
|
|
@@ -34,10 +52,15 @@ const syndicatePost = async ({
|
|
|
34
52
|
}) => {
|
|
35
53
|
// Readiness gate: verify post and OG image are live before syndicating.
|
|
36
54
|
// Skip check in force mode (manual re-syndication from UI/backlog script).
|
|
55
|
+
// Photo posts skip OG check — Eleventy deliberately skips OG generation
|
|
56
|
+
// for posts with photos (the photo itself serves as og:image in HTML).
|
|
37
57
|
const meUrl = typeof publication.me === "string" ? publication.me : publication.me?.href || publication.me?.toString?.() || "";
|
|
58
|
+
const hasPhotos = Array.isArray(postData.properties?.photo) && postData.properties.photo.length > 0;
|
|
38
59
|
if (!force && meUrl && postData.properties?.url) {
|
|
39
|
-
console.log(`[syndication] Readiness gate: checking ${postData.properties.url} (me=${meUrl})`);
|
|
40
|
-
const readiness =
|
|
60
|
+
console.log(`[syndication] Readiness gate: checking ${postData.properties.url} (me=${meUrl}, hasPhotos=${hasPhotos})`);
|
|
61
|
+
const readiness = hasPhotos
|
|
62
|
+
? await isPostReady(postData.properties.url, meUrl, { skipOg: true })
|
|
63
|
+
: await isPostReady(postData.properties.url, meUrl);
|
|
41
64
|
if (!readiness.ready) {
|
|
42
65
|
console.log(
|
|
43
66
|
`[syndication] Skipping ${postData.properties.url} — not yet built ` +
|
|
@@ -148,87 +171,98 @@ export const syndicateController = {
|
|
|
148
171
|
}
|
|
149
172
|
|
|
150
173
|
// --- Batch mode (no source_url — process ALL pending posts) ---
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (!allPostData || allPostData.length === 0) {
|
|
174
|
+
if (batchRunning) {
|
|
175
|
+
console.log("[syndication] Batch already in progress, skipping");
|
|
154
176
|
return response.json({
|
|
155
177
|
success: "OK",
|
|
156
|
-
success_description: "
|
|
178
|
+
success_description: "Batch already in progress",
|
|
157
179
|
});
|
|
158
180
|
}
|
|
159
181
|
|
|
160
|
-
|
|
161
|
-
`[syndication] Batch processing ${allPostData.length} post(s)`,
|
|
162
|
-
);
|
|
182
|
+
batchRunning = true;
|
|
163
183
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const result = await syndicatePost({
|
|
172
|
-
application,
|
|
173
|
-
publication,
|
|
174
|
-
postData,
|
|
175
|
-
bearerToken,
|
|
184
|
+
try {
|
|
185
|
+
const allPostData = await getAllPostData(postsCollection);
|
|
186
|
+
|
|
187
|
+
if (!allPostData || allPostData.length === 0) {
|
|
188
|
+
return response.json({
|
|
189
|
+
success: "OK",
|
|
190
|
+
success_description: "No posts awaiting syndication",
|
|
176
191
|
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
console.log(
|
|
195
|
+
`[syndication] Batch processing ${allPostData.length} post(s)`,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const results = [];
|
|
199
|
+
|
|
200
|
+
for (const [index, postData] of allPostData.entries()) {
|
|
201
|
+
try {
|
|
202
|
+
const result = await syndicatePost({
|
|
203
|
+
application,
|
|
204
|
+
publication,
|
|
205
|
+
postData,
|
|
206
|
+
bearerToken,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Post was skipped (not yet built)
|
|
210
|
+
if (result.skipped) {
|
|
211
|
+
results.push({
|
|
212
|
+
url: result.url,
|
|
213
|
+
success: true,
|
|
214
|
+
skipped: true,
|
|
215
|
+
reason: `Not yet built (post: ${result.readiness.postStatus}, og: ${result.readiness.ogStatus})`,
|
|
216
|
+
});
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
177
219
|
|
|
178
|
-
// Post was skipped (not yet built)
|
|
179
|
-
if (result.skipped) {
|
|
180
220
|
results.push({
|
|
181
221
|
url: result.url,
|
|
182
222
|
success: true,
|
|
183
|
-
|
|
184
|
-
|
|
223
|
+
syndicatedUrls: result.syndicatedUrls,
|
|
224
|
+
...(result.failedTargets && {
|
|
225
|
+
failedTargets: result.failedTargets,
|
|
226
|
+
}),
|
|
185
227
|
});
|
|
186
|
-
skippedCount++;
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
results.push({
|
|
191
|
-
url: result.url,
|
|
192
|
-
success: true,
|
|
193
|
-
syndicatedUrls: result.syndicatedUrls,
|
|
194
|
-
...(result.failedTargets && {
|
|
195
|
-
failedTargets: result.failedTargets,
|
|
196
|
-
}),
|
|
197
|
-
});
|
|
198
228
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
});
|
|
229
|
+
console.log(
|
|
230
|
+
`[syndication] Syndicated: ${result.url} (${result.syndicatedUrls.length} target(s))`,
|
|
231
|
+
);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
results.push({
|
|
234
|
+
url: postData.properties?.url,
|
|
235
|
+
success: false,
|
|
236
|
+
error: error.message,
|
|
237
|
+
});
|
|
209
238
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
239
|
+
console.error(
|
|
240
|
+
`[syndication] Failed: ${postData.properties?.url} - ${error.message}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
215
243
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
244
|
+
// Rate limit delay between posts (2 seconds)
|
|
245
|
+
if (index < allPostData.length - 1) {
|
|
246
|
+
await delay(2000);
|
|
247
|
+
}
|
|
219
248
|
}
|
|
220
|
-
}
|
|
221
249
|
|
|
222
|
-
|
|
223
|
-
|
|
250
|
+
const failed = results.filter((result) => !result.success).length;
|
|
251
|
+
const skipped = results.filter((result) => result.skipped).length;
|
|
252
|
+
const succeeded = results.length - failed - skipped;
|
|
253
|
+
const description =
|
|
254
|
+
`Processed ${allPostData.length} post(s): ${succeeded} succeeded, ${failed} failed, ${skipped} not yet built`;
|
|
224
255
|
|
|
225
|
-
|
|
256
|
+
console.log(`[syndication] ${description}`);
|
|
226
257
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
258
|
+
return response.json({
|
|
259
|
+
success: "OK",
|
|
260
|
+
success_description: description,
|
|
261
|
+
results,
|
|
262
|
+
});
|
|
263
|
+
} finally {
|
|
264
|
+
batchRunning = false;
|
|
265
|
+
}
|
|
232
266
|
} catch (error) {
|
|
233
267
|
next(error);
|
|
234
268
|
}
|
package/lib/utils.js
CHANGED
|
@@ -142,8 +142,12 @@ export const syndicateToTargets = async (
|
|
|
142
142
|
}
|
|
143
143
|
})
|
|
144
144
|
.map((t) => t.info.uid);
|
|
145
|
+
} else {
|
|
146
|
+
// Fall back to targets marked as checked by default
|
|
147
|
+
syndicateTo = syndicationTargets
|
|
148
|
+
.filter((t) => t?.options?.checked)
|
|
149
|
+
.map((t) => t.info.uid);
|
|
145
150
|
}
|
|
146
|
-
// If no existing syndication URLs, don't default to all — leave empty
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
// BUG FIX: Was `Array.isArray` (always truthy, it's a function reference)
|
|
@@ -254,12 +258,16 @@ export function urlToOgSlug(postUrl) {
|
|
|
254
258
|
*
|
|
255
259
|
* @param {string} postUrl - Full public URL of the post
|
|
256
260
|
* @param {string} me - Publication URL (e.g., "https://rmendes.net")
|
|
261
|
+
* @param {object} [options] - Options
|
|
262
|
+
* @param {boolean} [options.skipOg] - Skip OG image check (e.g., photo posts
|
|
263
|
+
* where Eleventy uses the photo itself as og:image instead of generating /og/*.png)
|
|
257
264
|
* @returns {Promise<{ready: boolean, postStatus: number, ogStatus: number}>}
|
|
258
265
|
*/
|
|
259
|
-
export async function isPostReady(postUrl, me) {
|
|
266
|
+
export async function isPostReady(postUrl, me, options = {}) {
|
|
267
|
+
const { skipOg = false } = options;
|
|
260
268
|
const ogSlug = urlToOgSlug(postUrl);
|
|
261
269
|
const meNorm = me?.replace(/\/$/, "");
|
|
262
|
-
const ogUrl = ogSlug && meNorm ? `${meNorm}/og/${ogSlug}.png` : null;
|
|
270
|
+
const ogUrl = !skipOg && ogSlug && meNorm ? `${meNorm}/og/${ogSlug}.png` : null;
|
|
263
271
|
|
|
264
272
|
const headCheck = async (url) => {
|
|
265
273
|
try {
|
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.39",
|
|
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",
|
|
@@ -37,5 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"test": "node --test test/*.js"
|
|
40
43
|
}
|
|
41
44
|
}
|