Merge pull request #1 from svemagie/claude/fix-activitypub-og-image-CrCGI
fix(ap): fix OG image not included in ActivityPub activities
This commit is contained in:
10
README.md
10
README.md
@@ -148,7 +148,7 @@ Posts are converted from Indiekit's JF2 format to ActivityStreams 2.0 in two mod
|
||||
- Permalink appended to content body
|
||||
- Nested hashtags normalized: `on/art/music` → `#music` (Mastodon doesn't support path-style tags)
|
||||
- Sensitive posts flagged with `sensitive: true`; summary doubles as CW text for notes
|
||||
- Per-post OG image added to Note/Article objects (`/og/{year}-{month}-{day}-{slug}.png`) for fediverse preview cards
|
||||
- Per-post OG image added to Note/Article objects (`/og/{slug}.png`) for fediverse preview cards
|
||||
|
||||
### Express ↔ Fedify bridge
|
||||
|
||||
@@ -160,15 +160,21 @@ Posts are converted from Indiekit's JF2 format to ActivityStreams 2.0 in two mod
|
||||
|
||||
### AP-specific patches
|
||||
|
||||
These patches are applied to `node_modules` via postinstall and at serve startup. They're needed because the lockfile pins the fork to v2.10.1 which predates some fixes, and because some fixes cannot be upstreamed.
|
||||
These patches are applied to `node_modules` via postinstall and at serve startup. They're needed because some fixes cannot be upstreamed or because they adapt upstream behaviour to this blog's specific URL structure.
|
||||
|
||||
| Patch | Target | What it does |
|
||||
|---|---|---|
|
||||
| `patch-ap-allow-private-address` | federation-setup.js | Adds `signatureTimeWindow` and `allowPrivateAddress` to `createFederation()` |
|
||||
| `patch-ap-url-lookup-api` | Adds new route | Public `GET /activitypub/api/ap-url` resolves blog URL → AP object URL |
|
||||
| `patch-ap-og-image` | jf2-to-as2.js | Fixes OG image URL generation — see below |
|
||||
| `patch-federation-unlisted-guards` | endpoint-syndicate | Prevents unlisted posts from being re-syndicated (AP fork has this natively) |
|
||||
| `patch-endpoint-activitypub-locales` | locales | Injects German (`de`) translations for the AP endpoint UI |
|
||||
|
||||
**`patch-ap-og-image.mjs`**
|
||||
The fork (both 842fc5af and 45f8ba9) attempts to derive the OG image path by matching a date-based URL pattern like `/articles/2024/01/15/slug/`. This blog uses flat URLs (`/articles/slug/`) with no date component, so the regex never matches and no `image` property is set on ActivityPub objects — Mastodon and other clients never show a preview card.
|
||||
|
||||
The patch replaces the broken date-from-URL regex with a simple last-path-segment extraction, producing `/og/{slug}.png` — the actual filename the Eleventy build generates (e.g. `/og/2615b.png`). Applied to both `jf2ToActivityStreams()` (plain JSON-LD) and `jf2ToAS2Activity()` (Fedify vocab objects).
|
||||
|
||||
### AP environment variables
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|
||||
136
scripts/patch-ap-og-image.mjs
Normal file
136
scripts/patch-ap-og-image.mjs
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Patch: fix OG image URL generation in ActivityPub jf2-to-as2.js.
|
||||
*
|
||||
* Root cause:
|
||||
* Both 842fc5af and 45f8ba9 versions of jf2-to-as2.js try to extract the
|
||||
* post slug from the URL using a regex that expects date-based URLs like
|
||||
* /articles/2024/01/15/slug/ but this blog uses flat URLs like /articles/slug/.
|
||||
* The regex never matches so the `image` property is never set — no OG image
|
||||
* preview card reaches Mastodon or other fediverse servers.
|
||||
*
|
||||
* Fix:
|
||||
* Replace the date-from-URL regex with a simple last-path-segment extraction.
|
||||
* Constructs /og/{slug}.png — the actual filename pattern the Eleventy build
|
||||
* generates for static OG preview images (e.g. /og/2615b.png).
|
||||
*
|
||||
* Both jf2ToActivityStreams() (plain JSON-LD) and jf2ToAS2Activity() (Fedify
|
||||
* vocab objects) are patched. Both 842fc5af and 45f8ba9 variants are handled
|
||||
* so the patch works regardless of which commit npm install resolved.
|
||||
*/
|
||||
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
const candidates = [
|
||||
"node_modules/@rmdes/indiekit-endpoint-activitypub/lib/jf2-to-as2.js",
|
||||
"node_modules/@indiekit/indiekit/node_modules/@rmdes/indiekit-endpoint-activitypub/lib/jf2-to-as2.js",
|
||||
];
|
||||
|
||||
const MARKER = "// og-image fix";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Use JS regex patterns to locate the OG image blocks.
|
||||
// Both 842fc5af and 45f8ba9 share the same variable names (ogMatch / ogMatchF)
|
||||
// and the same if-block structure, differing only in the URL construction.
|
||||
//
|
||||
// Pattern: matches from "const ogMatch[F] = postUrl && postUrl.match(" to the
|
||||
// closing "}" (2-space indent) of the if block.
|
||||
// ---------------------------------------------------------------------------
|
||||
const CN_BLOCK_RE =
|
||||
/ const ogMatch = postUrl && postUrl\.match\([^\n]+\n if \(ogMatch\) \{[\s\S]*?\n \}/;
|
||||
|
||||
const AS2_BLOCK_RE =
|
||||
/ const ogMatchF = postUrl && postUrl\.match\([^\n]+\n if \(ogMatchF\) \{[\s\S]*?\n \}/;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Replacement: extract slug from last URL path segment.
|
||||
// Build /og/{slug}.png to match the Eleventy OG filenames (e.g. /og/2615b.png).
|
||||
//
|
||||
// Template literal note: backslashes inside the injected regex are doubled so
|
||||
// they survive the template literal → string conversion:
|
||||
// \\\/ → \/ (escaped slash in regex)
|
||||
// [\\\w-] → [\w-] (word char class)
|
||||
// ---------------------------------------------------------------------------
|
||||
const NEW_CN = ` const ogSlug = postUrl && postUrl.match(/\\/([\\\w-]+)\\/?$/)?.[1]; // og-image fix
|
||||
if (ogSlug) { // og-image fix
|
||||
object.image = {
|
||||
type: "Image",
|
||||
url: \`\${publicationUrl.replace(/\\/$/, "")}/og/\${ogSlug}.png\`, // og-image fix
|
||||
mediaType: "image/png",
|
||||
};
|
||||
}`;
|
||||
|
||||
const NEW_AS2 = ` const ogSlugF = postUrl && postUrl.match(/\\/([\\\w-]+)\\/?$/)?.[1]; // og-image fix
|
||||
if (ogSlugF) { // og-image fix
|
||||
noteOptions.image = new Image({
|
||||
url: new URL(\`\${publicationUrl.replace(/\\/$/, "")}/og/\${ogSlugF}.png\`), // og-image fix
|
||||
mediaType: "image/png",
|
||||
});
|
||||
}`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function exists(filePath) {
|
||||
try {
|
||||
await access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let checked = 0;
|
||||
let patched = 0;
|
||||
|
||||
for (const filePath of candidates) {
|
||||
if (!(await exists(filePath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
checked += 1;
|
||||
const source = await readFile(filePath, "utf8");
|
||||
|
||||
if (source.includes(MARKER)) {
|
||||
console.log(`[postinstall] patch-ap-og-image: already applied to ${filePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let updated = source;
|
||||
let changed = false;
|
||||
|
||||
// Fix the jf2ToActivityStreams OG block
|
||||
if (CN_BLOCK_RE.test(updated)) {
|
||||
updated = updated.replace(CN_BLOCK_RE, NEW_CN);
|
||||
changed = true;
|
||||
} else {
|
||||
console.warn(
|
||||
`[postinstall] patch-ap-og-image: jf2ToActivityStreams OG block not found in ${filePath} — skipping`,
|
||||
);
|
||||
}
|
||||
|
||||
// Fix the jf2ToAS2Activity OG block
|
||||
if (AS2_BLOCK_RE.test(updated)) {
|
||||
updated = updated.replace(AS2_BLOCK_RE, NEW_AS2);
|
||||
changed = true;
|
||||
} else {
|
||||
console.warn(
|
||||
`[postinstall] patch-ap-og-image: jf2ToAS2Activity OG block not found in ${filePath} — skipping`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!changed || updated === source) {
|
||||
console.log(`[postinstall] patch-ap-og-image: no changes applied to ${filePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await writeFile(filePath, updated, "utf8");
|
||||
patched += 1;
|
||||
console.log(`[postinstall] Applied patch-ap-og-image to ${filePath}`);
|
||||
}
|
||||
|
||||
if (checked === 0) {
|
||||
console.log("[postinstall] patch-ap-og-image: no target files found");
|
||||
} else if (patched === 0) {
|
||||
console.log("[postinstall] patch-ap-og-image: already up to date");
|
||||
} else {
|
||||
console.log(`[postinstall] patch-ap-og-image: patched ${patched}/${checked} file(s)`);
|
||||
}
|
||||
Reference in New Issue
Block a user