Skip to content

Extract AT Protocol image caching to shared utility#359

Open
jaredpereira wants to merge 4 commits into
mainfrom
claude/publication-routes-image-audit-f2d6ln
Open

Extract AT Protocol image caching to shared utility#359
jaredpereira wants to merge 4 commits into
mainfrom
claude/publication-routes-image-audit-f2d6ln

Conversation

@jaredpereira

Copy link
Copy Markdown
Contributor

Summary

Refactors image caching logic for AT Protocol blobs into a reusable utility module (src/utils/atprotoImages.ts), eliminating duplication across multiple routes and enabling consistent image resizing/caching behavior.

Changes

New Files

  • src/utils/atprotoImages.ts: Centralized image caching machinery with three main exports:

    • fetchAtprotoBlob(): Fetches blobs from PDS given DID and CID
    • ensureOriginalCached(): Caches original PDS blobs to Supabase with proper Cache-Control headers
    • ensureDerivedImageCached(): Generic caching for derived images (resized variants, etc.)
    • ensureResizedVariantCached(): Resizes images with sharp and caches variants, with fallback to original
    • imageCacheUrl(): Constructs public URLs for cached images
  • src/utils/ogCoverImageRedirect.ts: New helper for OG image routes to redirect to cached cover variants instead of streaming

  • supabase/imageSizes.d.ts: TypeScript type definitions for image width tiers (360, 800, 1200, 2000)

Modified Files

API Routes:

  • app/api/atproto_images/route.ts: Simplified to use new utility functions, reducing ~60 lines of duplicated caching logic
  • app/api/pub_icon/route.ts: Refactored to use ensureDerivedImageCached() for icon resizing, now redirects to cached variants instead of streaming
  • app/(app)/lish/[did]/[publication]/icon/route.ts: Same refactoring for favicon generation

OG Image Routes:

  • app/(app)/lish/[did]/[publication]/[rkey]/opengraph-image.ts: Uses new coverImageRedirect() helper for single-hop redirect to cached cover images
  • app/(app)/p/[didOrHandle]/[rkey]/opengraph-image.ts: Same refactoring

Image Rendering:

  • src/utils/blobRefToSrc.ts: Added optional transform parameter to support image width tiers
  • app/(app)/lish/[did]/[publication]/[rkey]/PublishedImageGallery.tsx: Grid/strip cells now request 1200px tier; lightbox gets full-resolution
  • emails/post.tsx: Email images now request appropriately-sized variants (800px for images, 360px for website previews)
  • emails/standardSiteBlocks.tsx: Minor formatting cleanup
  • Multiple component files: Updated blobRefToSrc() calls to pass width transforms where appropriate

Other:

  • src/utils/uploadCoverImageThumb.ts: Import updated to use new utility location
  • app/api/link_previews/route.ts: Added cache control header for thumbnail storage
  • app/api/quote_screenshot/route.ts: Added edge caching headers
  • src/utils/screenshotPage.ts: Added CDN cache control header

Benefits

  1. Eliminates duplication: Image caching logic was previously scattered across three separate routes
  2. Consistent behavior: All image caching now uses the same machinery with proper Cache-Control headers
  3. Bandwidth optimization: Resized variants are cached once and served via CDN redirect instead of streaming through Vercel
  4. Responsive images: Components can now request appropriately-sized variants (e.g., 1200px for web, 800px for email) instead of always fetching full-resolution
  5. Fallback strategy: Routes gracefully fall back to streaming if caching fails, rather than failing entirely

Testing

  • Existing routes continue to work with simplified implementations
  • Image caching behavior is now centralized and testable
  • No new dependencies added

https://claude.ai/code/session_01TG1Z2RXovEdV9MZVqc5yDM

claude added 3 commits July 18, 2026 19:54
An audit of the image-serving paths found the /api/atproto_images and
/api/resized_images proxies working well (storage-cached variants served
by 302 off Supabase's CDN), but many call sites and a few routes
bypassing them:

- Post opengraph-image routes (lish + p) streamed the full-resolution
  cover blob from the PDS through Vercel with only a 1-hour cache. They
  now 302 to the cached proxy with a 1200px variant, cached a day at the
  edge (a republish can swap the cover at the same URL).
- /api/pub_icon and the per-publication favicon route re-fetched the
  blob from the PDS and re-ran sharp on every cache miss, streaming
  bytes through Vercel. Both now store the resized variant in storage
  keyed by CID (immutable) and 302 to it, falling back to streaming only
  if storage fails. The favicon also actually encodes to PNG now,
  matching its Content-Type.
- /api/quote_screenshot returned a multi-second remote-browser render
  with no Cache-Control at all, so the share modal's warm-up fetch saved
  nothing; it's now edge-cached for a day.
- ogScreenshotResponse gains a CDN-Cache-Control to match its s-maxage.
- link_previews thumbnails were stored with storage-js's default 1-hour
  cacheControl; now a day (not a year — the key is a url hash that gets
  upsert-overwritten on re-preview).

Call sites that shipped full-resolution blobs for small displays now
request downscaled variants via blobRefToSrc's transform: publication
icons everywhere (12-96px displays), wordmarks, website-block link
previews (120px), post/gallery/email body images, theme background
images (capped at the ladder max), and the dashboard's icon preview
(which also hand-built the proxy URL; it uses blobRefToSrc now).
Gallery lightboxes keep the full-resolution source.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TG1Z2RXovEdV9MZVqc5yDM
Two follow-ups to the image audit:

- Web post-body and gallery images were requesting the 800 tier, but the
  image-width ladder's 1200 tier exists precisely for document-body
  images at retina density (~600px column x 2) — at 800 photographs
  would render slightly soft on 2x displays. StaticPostContent and
  PublishedImageGallery now request 1200; the email renderers keep 800
  (smaller columns, proxied images) and lightboxes keep full resolution.

- The post OG cover path was a two-hop redirect chain (opengraph-image
  -> /api/atproto_images -> supabase.co), crossing origins on the second
  hop — og-image fetchers are flaky enough that chains risk broken
  unfurls. The variant-ensuring logic in /api/atproto_images is now
  extracted as ensureResizedVariantCached() and the OG routes redirect
  straight to the storage variant URL in one hop. This also restores the
  screenshot fallback when the cover blob can't be fetched, which the
  previous blind redirect had dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TG1Z2RXovEdV9MZVqc5yDM
- Move the blob-fetch and storage-cache machinery (fetchAtprotoBlob,
  ensureOriginalCached, ensureResizedVariantCached) out of the
  atproto_images route file into src/utils/atprotoImages, so library
  code no longer lives in (or is imported from) an HTTP endpoint module.
  The route is now a thin GET wrapper.
- Extract the shared store-and-redirect scaffolding the two icon routes
  each open-coded (public-URL builder, HEAD check, upload with the
  storage-js seconds quirk, failure fallback) into one primitive,
  ensureDerivedImageCached(path, produce); each icon route now supplies
  only its resize step and its own cache headers.
- Type transform widths as the ImageWidth union (360|800|1200|2000) via
  a d.ts companion to imageSizes.js, instead of naming each tier: call
  sites keep plain literals with no imports and the compiler rejects
  off-ladder values. The one computed width (wordmark) snaps explicitly.
- coverImageRedirect takes the blob ref and does the $link||toString()
  extraction itself instead of duplicating it in both OG routes, and
  drops a no-op snapToImageWidth(1200).
- PublishedImageGallery builds the full-resolution slide lazily in
  renderSlide instead of materializing a second parallel array that's
  unused until the lightbox opens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TG1Z2RXovEdV9MZVqc5yDM
@vercel

vercel Bot commented Jul 19, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
minilink Ready Ready Preview, Comment Jul 19, 2026 4:38am

Request Review

Conflict in DefaultPublicationHomepage.tsx: main removed the
buildPublicationPosts client fallback (#360) while this branch's
prettier pass had reflowed the same expression. Resolved to main's
version; the branch's iconUrl width transform is untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TG1Z2RXovEdV9MZVqc5yDM
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.

2 participants