Extract AT Protocol image caching to shared utility#359
Open
jaredpereira wants to merge 4 commits into
Open
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 CIDensureOriginalCached(): Caches original PDS blobs to Supabase with proper Cache-Control headersensureDerivedImageCached(): Generic caching for derived images (resized variants, etc.)ensureResizedVariantCached(): Resizes images with sharp and caches variants, with fallback to originalimageCacheUrl(): Constructs public URLs for cached imagessrc/utils/ogCoverImageRedirect.ts: New helper for OG image routes to redirect to cached cover variants instead of streamingsupabase/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 logicapp/api/pub_icon/route.ts: Refactored to useensureDerivedImageCached()for icon resizing, now redirects to cached variants instead of streamingapp/(app)/lish/[did]/[publication]/icon/route.ts: Same refactoring for favicon generationOG Image Routes:
app/(app)/lish/[did]/[publication]/[rkey]/opengraph-image.ts: Uses newcoverImageRedirect()helper for single-hop redirect to cached cover imagesapp/(app)/p/[didOrHandle]/[rkey]/opengraph-image.ts: Same refactoringImage Rendering:
src/utils/blobRefToSrc.ts: Added optionaltransformparameter to support image width tiersapp/(app)/lish/[did]/[publication]/[rkey]/PublishedImageGallery.tsx: Grid/strip cells now request 1200px tier; lightbox gets full-resolutionemails/post.tsx: Email images now request appropriately-sized variants (800px for images, 360px for website previews)emails/standardSiteBlocks.tsx: Minor formatting cleanupblobRefToSrc()calls to pass width transforms where appropriateOther:
src/utils/uploadCoverImageThumb.ts: Import updated to use new utility locationapp/api/link_previews/route.ts: Added cache control header for thumbnail storageapp/api/quote_screenshot/route.ts: Added edge caching headerssrc/utils/screenshotPage.ts: Added CDN cache control headerBenefits
Testing
https://claude.ai/code/session_01TG1Z2RXovEdV9MZVqc5yDM