@rmdes/indiekit-endpoint-syndicate 1.0.0-beta.34 → 1.0.0-beta.36
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 +30 -1
- package/lib/utils.js +71 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import { findBearerToken } from "../token.js";
|
|
|
4
4
|
import {
|
|
5
5
|
getAllPostData,
|
|
6
6
|
getPostData,
|
|
7
|
+
isPostReady,
|
|
7
8
|
syndicateToTargets,
|
|
8
9
|
} from "../utils.js";
|
|
9
10
|
|
|
@@ -31,6 +32,21 @@ const syndicatePost = async ({
|
|
|
31
32
|
bearerToken,
|
|
32
33
|
force = false,
|
|
33
34
|
}) => {
|
|
35
|
+
// Readiness gate: verify post and OG image are live before syndicating.
|
|
36
|
+
// Skip check in force mode (manual re-syndication from UI/backlog script).
|
|
37
|
+
const meUrl = typeof publication.me === "string" ? publication.me : publication.me?.href || publication.me?.toString?.() || "";
|
|
38
|
+
if (!force && meUrl && postData.properties?.url) {
|
|
39
|
+
console.log(`[syndication] Readiness gate: checking ${postData.properties.url} (me=${meUrl})`);
|
|
40
|
+
const readiness = await isPostReady(postData.properties.url, meUrl);
|
|
41
|
+
if (!readiness.ready) {
|
|
42
|
+
console.log(
|
|
43
|
+
`[syndication] Skipping ${postData.properties.url} — not yet built ` +
|
|
44
|
+
`(post: ${readiness.postStatus}, og: ${readiness.ogStatus})`
|
|
45
|
+
);
|
|
46
|
+
return { skipped: true, url: postData.properties.url, readiness };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
const { failedTargets, syndicatedUrls } = await syndicateToTargets(
|
|
35
51
|
publication,
|
|
36
52
|
postData.properties,
|
|
@@ -148,6 +164,7 @@ export const syndicateController = {
|
|
|
148
164
|
const results = [];
|
|
149
165
|
let successCount = 0;
|
|
150
166
|
let failCount = 0;
|
|
167
|
+
let skippedCount = 0;
|
|
151
168
|
|
|
152
169
|
for (const postData of allPostData) {
|
|
153
170
|
try {
|
|
@@ -158,6 +175,18 @@ export const syndicateController = {
|
|
|
158
175
|
bearerToken,
|
|
159
176
|
});
|
|
160
177
|
|
|
178
|
+
// Post was skipped (not yet built)
|
|
179
|
+
if (result.skipped) {
|
|
180
|
+
results.push({
|
|
181
|
+
url: result.url,
|
|
182
|
+
success: true,
|
|
183
|
+
skipped: true,
|
|
184
|
+
reason: `Not yet built (post: ${result.readiness.postStatus}, og: ${result.readiness.ogStatus})`,
|
|
185
|
+
});
|
|
186
|
+
skippedCount++;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
161
190
|
results.push({
|
|
162
191
|
url: result.url,
|
|
163
192
|
success: true,
|
|
@@ -191,7 +220,7 @@ export const syndicateController = {
|
|
|
191
220
|
}
|
|
192
221
|
|
|
193
222
|
const description =
|
|
194
|
-
`Processed ${allPostData.length} post(s): ${successCount} succeeded, ${failCount} failed`;
|
|
223
|
+
`Processed ${allPostData.length} post(s): ${successCount} succeeded, ${failCount} failed, ${skippedCount} not yet built`;
|
|
195
224
|
|
|
196
225
|
console.log(`[syndication] ${description}`);
|
|
197
226
|
|
package/lib/utils.js
CHANGED
|
@@ -217,3 +217,74 @@ export const syndicateToTargets = async (
|
|
|
217
217
|
syndicatedUrls,
|
|
218
218
|
};
|
|
219
219
|
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Derive OG image slug from a post URL path.
|
|
223
|
+
* Matches Eleventy theme's ogSlug filter logic.
|
|
224
|
+
*
|
|
225
|
+
* Date-based: /type/YYYY/MM/DD/slug → YYYY-MM-DD-slug
|
|
226
|
+
* Non-date: /about/ → about
|
|
227
|
+
*
|
|
228
|
+
* @param {string} postUrl - Full post URL
|
|
229
|
+
* @returns {string|null} OG slug or null
|
|
230
|
+
*/
|
|
231
|
+
export function urlToOgSlug(postUrl) {
|
|
232
|
+
try {
|
|
233
|
+
const pathname = new URL(postUrl).pathname;
|
|
234
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
235
|
+
|
|
236
|
+
// Date-based URL: /type/yyyy/MM/dd/slug → 5 segments
|
|
237
|
+
if (segments.length === 5) {
|
|
238
|
+
const [, year, month, day, slug] = segments;
|
|
239
|
+
if (/^\d{4}$/.test(year) && /^\d{2}$/.test(month) && /^\d{2}$/.test(day)) {
|
|
240
|
+
return `${year}-${month}-${day}-${slug}`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Fallback: last segment
|
|
245
|
+
return segments[segments.length - 1] || null;
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check if a post and its OG image are live on the public site.
|
|
253
|
+
* Uses HTTP HEAD requests with a 5-second timeout.
|
|
254
|
+
*
|
|
255
|
+
* @param {string} postUrl - Full public URL of the post
|
|
256
|
+
* @param {string} me - Publication URL (e.g., "https://rmendes.net")
|
|
257
|
+
* @returns {Promise<{ready: boolean, postStatus: number, ogStatus: number}>}
|
|
258
|
+
*/
|
|
259
|
+
export async function isPostReady(postUrl, me) {
|
|
260
|
+
const ogSlug = urlToOgSlug(postUrl);
|
|
261
|
+
const meNorm = me?.replace(/\/$/, "");
|
|
262
|
+
const ogUrl = ogSlug && meNorm ? `${meNorm}/og/${ogSlug}.png` : null;
|
|
263
|
+
|
|
264
|
+
const headCheck = async (url) => {
|
|
265
|
+
try {
|
|
266
|
+
const controller = new AbortController();
|
|
267
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
268
|
+
const response = await fetch(url, {
|
|
269
|
+
method: "HEAD",
|
|
270
|
+
redirect: "follow",
|
|
271
|
+
signal: controller.signal,
|
|
272
|
+
});
|
|
273
|
+
clearTimeout(timeoutId);
|
|
274
|
+
return response.status;
|
|
275
|
+
} catch {
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const [postStatus, ogStatus] = await Promise.all([
|
|
281
|
+
headCheck(postUrl),
|
|
282
|
+
ogUrl ? headCheck(ogUrl) : Promise.resolve(200),
|
|
283
|
+
]);
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
ready: postStatus === 200 && ogStatus === 200,
|
|
287
|
+
postStatus,
|
|
288
|
+
ogStatus,
|
|
289
|
+
};
|
|
290
|
+
}
|
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.36",
|
|
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",
|