Skip to content

Repository files navigation

og-preview

Check how a URL's social share card renders — by fetching it once per platform, not once total.

MIT

Why this exists

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.

Packages

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.

Usage

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.

In React

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.

Three outcomes, deliberately

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.

Security

@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 http and https only
  • 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.

Development

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       # biome

License

MIT

About

Check how a URL's social share card renders on every platform — each one fetched separately, so UA-gated preview bugs actually show up.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages