Skip to content

proxy + self-host og:image and favicon for link cards

jedmund requested to merge jedmund/og-image-proxy into main

Created by: jedmund

Summary

  • adds GET /api/og-image-proxy?url=<encoded> that streams remote image bytes with strict guards (http(s) only, private/loopback hosts blocked, image content-type only, 10 MB ceiling, 8s timeout, 1-day cache)
  • GET /api/og-metadata now rewrites both the image and favicon fields to proxy urls before returning
  • enrichUrlEmbeds now downloads the favicon in parallel with the main image and stores it as a Media row with faviconMediaId on the urlEmbed node
  • legacy posts with foreign favicon urls self-heal on re-save (same pattern as the image path)

Why

after #82 the editor still showed raw third-party og:image urls during editing, which our img-src 'self' ... CSP blocks. Same issue hits legacy posts on public pages saved before #82, and the favicon path had the exact same hotlink problem that the image path had.

Deployment note

#82's migration (20260422000000_add_media_link_card_flag) must be applied before this change is useful — without it, save-time Media.create() throws because isLinkCardImage doesn't exist in the DB, so downloads silently fall through to the hotlink path.

npm run db:deploy

Test plan

  • editor: paste a url, confirm both the preview image and favicon load via /api/og-image-proxy?url=... and no CSP violation fires
  • save the post, confirm content.urlEmbed.attrs.image and .favicon are both /api/media/<id> urls
  • confirm new Media rows for both have isLinkCardImage = true (so they stay hidden from the library)
  • open a post saved before #82 (raw hotlinks in attrs) — proxy handles the render; re-saving upgrades both to hosted urls
  • broken/403 source favicon → save still succeeds; attrs keep whatever was there as fallback
  • /api/og-image-proxy?url=http://localhost:22 → 400 disallowed
  • /api/og-image-proxy?url=file:///etc/passwd → 400 unsupported scheme
  • /api/og-image-proxy?url=https://example.com/huge.png with content-length > 10MB → 413
  • /api/og-image-proxy?url=https://example.com/page.html (non-image) → 415

Merge request reports

Loading