Skip to content

fix(preview): serve Open Graph to iMessage, Reddit and Bluesky scrapers (SIT-359) - #42

Open
onamfc wants to merge 1 commit into
mainfrom
brandon/sit-359-core-scraper-allowlist-stale
Open

fix(preview): serve Open Graph to iMessage, Reddit and Bluesky scrapers (SIT-359)#42
onamfc wants to merge 1 commit into
mainfrom
brandon/sit-359-core-scraper-allowlist-stale

Conversation

@onamfc

@onamfc onamfc commented Aug 25, 2026

Copy link
Copy Markdown
Member

SIT-359. Brings core's scraper allowlist back in line with Cloud's, which moved ahead in cloud#217 (SIT-356).

The gap

isSocialScraper (src/routes/preview.ts) has held the same 13 user-agents since the beginning. Anything unlisted falls through to the redirect handler, so the share card renders the destination site's metadata instead of the link's.

Cloud verified against production that three real scrapers were affected:

User-agent Before
Applebot/0.1 (iMessage, Notes, Spotlight) 302 → destination
redditbot/1.0 302 → destination
Bluesky Cardyb/1.1 302 → destination

Self-hosted only. Cloud registers its own hook ahead of previewRoutes, so Cloud behaviour is governed by its copy and is already fixed. This is the parity fix for everyone running core directly.

Why Bluesky is the urgent one

CardyB bakes the fetched card into the post record permanently. Unlike Facebook or LinkedIn, there is no force-re-scrape — a post that captured a wrong card keeps it forever. Its user-agent masquerades as Chrome, so Cardyb is the only usable token:

Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible;
Bluesky Cardyb/1.1; +mailto:support@bsky.app) Chrome/W.X.Y.Z Safari/537.36

What is added

Applebot, redditbot, Cardyb, Mastodon, vkShare, Embedly, Iframely, plus the common validators (opengraph, MetaInspector, PreviewBot, LinkPreview). Every entry is a token only a server-side fetcher sends.

What is deliberately NOT added

Instagram, Threads, Snapchat, Tumblr. Each of those strings appears in that app's in-app browser user-agent, so matching the bare name would serve a real person the meta-refresh interstitial instead of their redirect — worse than a missing preview card.

Instagram and Threads previews are generated by Meta's facebookexternalhit, already in the list, so adding them buys nothing and only creates the regression. A test asserts all four stay absent, with the reason attached.

WhatsApp has the same defect and predates the rule. Left alone deliberately — narrowing it risks breaking previews that currently work. Documented in a comment.

Structural change

scraperPatterns moves from a function-local array to a module-level SCRAPER_PATTERNS, exported alongside isSocialScraper so both are testable. Behaviour and signature are unchanged.

Verification

  • npm run build clean, tsc --noEmit clean.
  • npx vitest run232 tests across 15 files (was 198 across 14). The 34 new cases cover every scraper pattern, every human UA including the four in-app browsers, and the absent-pattern guard.
  • Diff is 2 files.

Note on the duplication

Cloud keeps its own copy at backend/src/lib/social-preview-hook.ts and must keep overriding this one. Consolidating into a shared export was considered and rejected: it would mean publishing core and bumping Cloud for every pattern addition. The two lists are kept in sync by hand, and the test suites are intentionally near-identical so a drift is visible.

…rs (SIT-359)

Core's scraper allowlist had drifted behind Cloud's. It held the same 13
user-agents it has since the beginning, so self-hosted deployments still
redirect Applebot, redditbot and Bluesky's Cardyb to the destination
instead of serving them the preview page — the share card then shows the
destination site's metadata rather than the link's.

Cloud verified all three against production and fixed its own copy in
cloud#217. Cloud registers its hook ahead of these routes, so this list
governs self-hosted deployments only.

Bluesky is the one worth prioritising: CardyB bakes the fetched card into
the post record permanently, so a post that captured a wrong card can
never be re-scraped the way a Facebook or LinkedIn share can.

Adds Applebot, redditbot, Cardyb, Mastodon, vkShare, the Embedly and
Iframely aggregators, and the common preview validators. Every entry is a
token only a server-side fetcher sends.

Deliberately NOT added: Instagram, Threads, Snapchat, Tumblr. Each name
appears in that app's in-app browser user-agent, so matching it would
serve a real person the meta-refresh interstitial instead of their
redirect. Instagram and Threads previews come from facebookexternalhit,
already covered. A test pins all four as non-matches.

Lifts the list to a module-level SCRAPER_PATTERNS and exports it with
isSocialScraper so both are testable. First test coverage for this gate:
232 tests pass, tsc clean.
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant