Share the link-preview fallback between the Meta adapters

Instagram and Threads both ended with the same twelve lines: read the Open
Graph tags, make og:video a video with og:image as its poster, otherwise make
og:image an image, and attach the referrer headers the CDN wants. Facebook
needs the same floor for the same reason, and three copies of a thing is where
it stops being a coincidence.

meta-media.ts already exists because these platforms are one product
underneath. The media schema it normalizes is Instagram's and Threads';
Facebook's is its own, but the headers and the link preview are shared with
both, so that is where this goes.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-12 11:35:16 -03:00
co-authored by Claude Opus 5
parent 635bc9d0a4
commit fa07b43c1c
4 changed files with 67 additions and 34 deletions
+8 -16
View File
@@ -1,6 +1,12 @@
import { ResolveError, oneSegment, type Media, type Post } from '../types.ts';
import { readOpenGraph } from '../browser/capture.ts';
import { findCaption, findMetaMedia, mediaFromMetaNode, metaHeaders } from './meta-media.ts';
import {
findCaption,
findMetaMedia,
mediaFromMetaNode,
mediaFromOpenGraph,
metaHeaders,
} from './meta-media.ts';
import { extractJsonObjectAfterKey } from './scan.ts';
import { hostMatcher } from './url.ts';
import type { PlatformSpec, ResolveContext } from './types.ts';
@@ -202,22 +208,8 @@ function fromPayloads(texts: string[]): Scraped | undefined {
/** Absolute floor: whatever the page offers a link preview. */
async function fromOpenGraph(ctx: ResolveContext): Promise<Scraped> {
const og = await readOpenGraph(ctx.page);
const fetchHeaders = metaHeaders(REFERER);
const media: Media[] = [];
if (og['og:video']) {
media.push({
kind: 'video',
url: og['og:video'],
fetchHeaders,
...(og['og:image'] ? { poster: { url: og['og:image'], fetchHeaders } } : {}),
});
} else if (og['og:image']) {
media.push({ kind: 'image', url: og['og:image'], fetchHeaders });
}
return {
media,
media: mediaFromOpenGraph(og, REFERER),
...(og['og:description'] ? { text: og['og:description'] } : {}),
};
}
+25 -2
View File
@@ -1,8 +1,10 @@
import type { Media } from '../types.ts';
/**
* Instagram and Threads are the same product underneath and serve the same
* media schema, so both adapters normalize through here.
* Instagram, Threads and Facebook are the same product underneath. The first
* two serve the same media schema, so both normalize through here; Facebook's
* is its own, but the headers its CDN wants and the link preview it falls
* back to are shared with them.
*/
type Candidate = { url?: string; width?: number; height?: number };
@@ -66,6 +68,27 @@ export function mediaFromMetaNode(node: MetaMediaNode, referer: string): Media[]
return [];
}
/**
* The link preview as media: the floor on all three, and all that is left
* when a post is refused to a logged-out reader.
*/
export function mediaFromOpenGraph(og: Record<string, string>, referer: string): Media[] {
const fetchHeaders = metaHeaders(referer);
if (og['og:video']) {
return [{
kind: 'video',
url: og['og:video'],
fetchHeaders,
...(og['og:image'] ? { poster: { url: og['og:image'], fetchHeaders } } : {}),
}];
}
if (og['og:image']) {
return [{ kind: 'image', url: og['og:image'], fetchHeaders }];
}
return [];
}
function nonEmptyArray(value: unknown): boolean {
return Array.isArray(value) && value.length > 0;
}
+9 -15
View File
@@ -1,6 +1,12 @@
import { ResolveError, type Media, type Post, type Segment } from '../types.ts';
import { ResolveError, type Post, type Segment } from '../types.ts';
import { readOpenGraph } from '../browser/capture.ts';
import { findCaption, findMetaMedia, mediaFromMetaNode, metaHeaders, type MetaMediaNode } from './meta-media.ts';
import {
findCaption,
findMetaMedia,
mediaFromMetaNode,
mediaFromOpenGraph,
type MetaMediaNode,
} from './meta-media.ts';
import { hostMatcher } from './url.ts';
import type { PlatformSpec, ResolveContext } from './types.ts';
@@ -129,19 +135,7 @@ function segmentOf(post: ThreadsPost, isAnchor: boolean): Segment {
/** The floor, when the structured payload is not there to be read. */
async function fromOpenGraph(ctx: ResolveContext): Promise<Segment | undefined> {
const og = await readOpenGraph(ctx.page);
const fetchHeaders = metaHeaders(REFERER);
const media: Media[] = [];
if (og['og:video']) {
media.push({
kind: 'video',
url: og['og:video'],
fetchHeaders,
...(og['og:image'] ? { poster: { url: og['og:image'], fetchHeaders } } : {}),
});
} else if (og['og:image']) {
media.push({ kind: 'image', url: og['og:image'], fetchHeaders });
}
const media = mediaFromOpenGraph(og, REFERER);
const text = og['og:description'];
if (media.length === 0 && !text) return undefined;