Check how a URL's social share card renders — by fetching it once per platform, not once total.
Every other Open Graph inspector fetches a URL one time and re-renders that single response inside different card frames. That works right up until a site answers different scrapers differently — and then the tool reports a card that nobody will ever actually see.
That is not a hypothetical. We found it in our own product: a link served a
complete Open Graph page to facebookexternalhit while Applebot, redditbot
and Bluesky's Cardyb were redirected to the destination site, so iMessage,
Reddit and Bluesky cards silently rendered the wrong site's metadata. Every
single-fetch validator we checked reported the link as fine, because they only
ever asked as one platform.
og-preview asks as each of them, separately, and tells you when the answers disagree.
It also matters more than it looks on Bluesky specifically: CardyB bakes the fetched card into the post record permanently. There is no re-scrape. A post that captured the wrong card keeps it forever.
| Package | What it is |
|---|---|
@linkforty/og-core |
Runtime-agnostic engine. Platform user-agents, HTML metadata parsing, agreement calculation. No I/O, no dependencies. |
@linkforty/og-node |
Node fetcher, hardened against SSRF. Safe to point at untrusted URLs. |
@linkforty/og-react |
<OgPreview /> — drop-in inspector UI. |
Core does no network access of its own. It takes a fetcher by injection, which is what lets one engine serve Node today and other runtimes later without the security model leaking into shared code.
import { inspect } from '@linkforty/og-core';
import { createNodeFetcher } from '@linkforty/og-node';
const result = await inspect('https://example.com/page', {
fetcher: createNodeFetcher(),
});
if (!result.agreement.identical) {
console.log('Platforms disagree on:', result.agreement.differingFields);
}
for (const platform of result.platforms) {
console.log(platform.platform.label, platform.outcome, platform.tags?.title);
}Each platform result carries its full redirect chain, so a scraper being sent somewhere else shows up directly rather than having to be inferred.
import { OgPreview } from '@linkforty/og-react';
<OgPreview endpoint="/api/og-inspect" />;The component owns the input, the request and the rendering; you supply the endpoint. Stand up your own backend rather than depending on someone else's service.
Every platform result is ok, blocked, or error.
blocked means the site refused that user-agent — a 401, 403 or 429 from a bot
filter. It is kept strictly separate from error, and excluded from the
agreement calculation entirely. A large share of the web answers unknown bots
with a 403, and reporting that as "this platform sees different metadata" would
make the tool wrong on perfectly ordinary sites.
@linkforty/og-node is built to sit behind a public, unauthenticated endpoint
that accepts URLs from strangers. That is a textbook SSRF position, so the
fetcher:
- allows
httpandhttpsonly - resolves every hostname and refuses loopback, private, link-local, unique-local, unspecified and IPv4-mapped addresses
- re-runs that check on every redirect hop — a first-URL-only check is defeated by any open redirect
- caps hops, request time, and how much of the body it reads
- forwards no cookies, credentials or headers from the calling request
One limitation, accepted knowingly: DNS rebinding between resolution and fetch
is not closed. The impact is bounded to one truncated GET body, and closing it
properly requires pinning the resolved IP and overriding the TLS SNI host. See
the note in packages/node/src/fetcher.ts.
Metadata returned by this library comes from arbitrary third-party pages. Treat every string as untrusted text: escape it on output, and never inject it as HTML.
pnpm install
pnpm build # build packages in dependency order
pnpm test # vitest
pnpm typecheck # requires a prior build — packages resolve each other via dist/
pnpm lint # biomeMIT