Skip to content

Serve listing photos with responsive srcset variants

jedmund requested to merge jedmund/responsive-images into main

Summary

  • Generate 400/800/1600 px sidecar JPEGs at upload time in one Sharp pipeline; canonical 2000 px key unchanged (no DB migration).
  • <img srcset sizes> on ListingCard (sizes="(min-width: 640px) 240px, 50vw") and ImageCarousel (sizes="100vw"). OG/Twitter meta still uses the canonical URL — scrapers want one big image.
  • deleteImage removes all three sidecars alongside the canonical (best-effort).
  • One-shot backfill script: pnpm backfill:image-variants — iterates images rows, HeadObjects to skip already-backfilled rows, downloads canonical, regenerates and uploads missing sidecars.
  • ListingCard hover-preloader now sets srcset/sizes on new Image() so it preloads the thumbnail variant instead of the 2 MB canonical.

Why

Every <img> previously pulled the full 2000 px / ~2 MB JPEG even when rendered inside a ~220 px grid cell. On mobile or slow connections that's significant wasted bandwidth and a visible perf hit.

Test plan

  • pnpm install (new tsx devDep) then pnpm check — passes 0 errors / 0 warnings.
  • pnpm build — clean.
  • pnpm dev, upload a new photo via /admin/new, confirm S3 / RustFS holds four objects: canonical + -w400 + -w800 + -w1600.
  • Load / in DevTools → Network → Img, throttle "Slow 4G". Thumbnails should fetch -w400 or -w800, not the canonical.
  • Load /sale/{shortId} on a 1920-wide viewport — carousel fetches -w1600; resize to mobile — falls to -w800.
  • Delete an image via /admin/edit/{id} — all four S3 objects should be removed.
  • pnpm backfill:image-variants against a DB with existing images — sidecars appear; re-running is a no-op.
  • curl -s https://<host>/sale/{shortId} | grep og:image — still points at the canonical 2000 px URL.

🤖 Generated with Claude Code

Merge request reports

Loading