Skip to content

self-host og:image thumbnails for link cards

jedmund requested to merge jedmund/self-host-link-images into main

Created by: jedmund

Summary

  • download og:images at post save time and store them as Media rows (cloudinary in prod, local disk in dev), then point the urlEmbed node at the self-hosted url
  • new isLinkCardImage flag on Media keeps these auto-downloaded thumbnails out of the media library
  • extend rich-text media walker to pick up urlEmbed.imageMediaId so link-card assets show up in media.usedIn and don't look orphaned

Why

after #81 the editor was saving og:image hotlink urls directly in the post json. hotlinking is fragile: hosts block based on referer, CSPs trip, images get removed or renamed, and we leak reader ip to third parties. self-hosting eliminates link rot and makes link cards behave like any other image asset in the app.

Migration

adds Media.isLinkCardImage (boolean, default false) + index. run npm run db:migrate in dev, npm run db:deploy in prod.

Existing posts

re-saving a post with a foreign image url will trigger the download and swap in the hosted url automatically — no backfill script needed. posts with cloudinary/local image urls already are treated as locally hosted and skipped.

Test plan

  • paste a url with a valid og:image and save — the saved content has a cloudinary / /local-uploads/ url, not the origin url
  • confirm the new Media row has isLinkCardImage = true
  • GET /api/media does not return link-card rows
  • source url with broken/403 og:image — save succeeds; attrs keep the original hotlink as fallback
  • image > 10MB or non-image content-type — download is skipped, hotlink is kept
  • open an existing post saved under #81 with a hotlink image, save it again, confirm the image attr becomes a hosted url
  • deleting a post decrements media.usedIn via existing MediaUsage tracking for the link-card row

Merge request reports

Loading