@rmdes/indiekit-endpoint-syndicate 1.0.0-beta.44 → 1.0.0-beta.45

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.
Files changed (2) hide show
  1. package/lib/utils.js +49 -50
  2. package/package.json +1 -1
package/lib/utils.js CHANGED
@@ -221,38 +221,6 @@ export const syndicateToTargets = async (
221
221
  };
222
222
  };
223
223
 
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
224
 
257
225
  /**
258
226
  * HEAD a URL and report its status, 0 on network error or timeout
@@ -276,30 +244,61 @@ const headCheck = async (url) => {
276
244
  };
277
245
 
278
246
  /**
279
- * Check if a post and its OG image are live on the public site.
280
- * Uses HTTP HEAD requests with a 5-second timeout.
247
+ * GET a page and return its status plus the og:image it declares.
248
+ * @param {string} url - Page URL
249
+ * @returns {Promise<{status: number, ogImage: string|undefined}>}
250
+ */
251
+ const fetchOgImage = async (url) => {
252
+ try {
253
+ const controller = new AbortController();
254
+ const timeoutId = setTimeout(() => controller.abort(), 5000);
255
+ const response = await fetch(url, { redirect: "follow", signal: controller.signal });
256
+ clearTimeout(timeoutId);
257
+ if (response.status !== 200) return { status: response.status };
258
+ const html = await response.text();
259
+ const match = html.match(/<meta\s+property="og:image"\s+content="([^"]+)"/);
260
+ return { status: 200, ogImage: match?.[1] };
261
+ } catch {
262
+ return { status: 0 };
263
+ }
264
+ };
265
+
266
+ /**
267
+ * Check if a post and its OG card are live on the public site.
268
+ *
269
+ * Reads the og:image the POST ITSELF declares rather than re-deriving its
270
+ * filename. The removed helper's own docstring said "Matches Eleventy theme's
271
+ * ogSlug filter logic" — a hand-copied convention from another repo, and on
272
+ * 2026-09-13 that convention changed: the theme added the post type to the
273
+ * cache key so a like and a reply sharing a basename stop colliding. This gate
274
+ * kept asking for the old type-less name, got 404 for cards that were
275
+ * demonstrably live, and silently blocked syndication for every new post.
276
+ *
277
+ * Reading the page removes the coupling: whatever the page says its card is, we
278
+ * check that. It also makes `skipOg` unnecessary in principle — a photo post
279
+ * declares its photo as og:image and that HEADs the same way — though the flag
280
+ * is kept for callers that still pass it.
281
+ *
282
+ * Costs one GET instead of one HEAD, for posts awaiting syndication only.
281
283
  * @param {string} postUrl - Full public URL of the post
282
- * @param {string} me - Publication URL (e.g., "https://rmendes.net")
284
+ * @param {string} me - Publication URL (no longer used to build the card URL; kept for the signature)
283
285
  * @param {object} [options] - Options
284
- * @param {boolean} [options.skipOg] - Skip OG image check (e.g., photo posts
285
- * where Eleventy uses the photo itself as og:image instead of generating /og/*.png)
286
+ * @param {boolean} [options.skipOg] - Don't wait on any og:image
286
287
  * @returns {Promise<{ready: boolean, postStatus: number, ogStatus: number}>}
287
288
  */
288
289
  export async function isPostReady(postUrl, me, options = {}) {
289
290
  const { skipOg = false } = options;
290
- const ogSlug = urlToOgSlug(postUrl);
291
- const meNorm = me?.replace(/\/$/, "");
292
- const ogUrl =
293
- !skipOg && ogSlug && meNorm ? `${meNorm}/og/${ogSlug}.png` : undefined;
291
+ const { status: postStatus, ogImage } = await fetchOgImage(postUrl);
294
292
 
295
- const [postStatus, ogStatus] = await Promise.all([
296
- headCheck(postUrl),
297
- ogUrl ? headCheck(ogUrl) : Promise.resolve(200),
298
- ]);
293
+ if (postStatus !== 200) {
294
+ return { ready: false, postStatus, ogStatus: 0 };
295
+ }
296
+ // No card declared (or explicitly skipped) → nothing to wait for.
297
+ if (skipOg || !ogImage) {
298
+ return { ready: true, postStatus, ogStatus: 200 };
299
+ }
299
300
 
300
- return {
301
- ready: postStatus === 200 && ogStatus === 200,
302
- postStatus,
303
- ogStatus,
304
- };
301
+ const ogStatus = await headCheck(ogImage);
302
+ return { ready: ogStatus === 200, postStatus, ogStatus };
305
303
  }
304
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-syndicate",
3
- "version": "1.0.0-beta.44",
3
+ "version": "1.0.0-beta.45",
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",