this repo has no description
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: proxy banner blob for OG embed; drop share description text

Made-with: Cursor

+43 -20
+37 -16
routes/api/registry/banner/[did].ts
··· 1 1 /** 2 - * Compatibility endpoint for registry profile banners. Mirrors the 3 - * avatar route — primary UI paths can hit Bluesky's CDN directly via 4 - * `bskyCdnBannerUrl`; this route exists so external link unfurlers, 5 - * old embeds, or any consumer that just knows the DID can resolve the 6 - * banner without first looking up the cid. 2 + * Proxies a project's banner blob from the owner's PDS. Used as the 3 + * canonical banner URL for both the in-page <img> and the OG/Twitter 4 + * meta image so link-unfurlers (Bluesky, Slack, Twitter, iMessage, etc.) 5 + * can fetch it directly without cross-origin PDS restrictions. 6 + * 7 + * The response is aggressively cached — the cache key includes the DID 8 + * (stable) but not the CID, so cache-control is bounded the same way as 9 + * the screenshot proxy: long enough to be useful, short enough that a 10 + * banner replacement shows up within a day. 7 11 */ 8 12 import { define } from "../../../../utils.ts"; 9 - import { bskyCdnBannerUrl } from "../../../../lib/avatar.ts"; 10 13 import { getProfileByDid } from "../../../../lib/registry.ts"; 14 + import { fetchBlobPublic } from "../../../../lib/pds.ts"; 11 15 import { withRateLimit } from "../../../../lib/rate-limit.ts"; 12 16 13 17 export const handler = define.handlers({ 14 18 GET: withRateLimit(async (ctx) => { 15 19 const did = decodeURIComponent(ctx.params.did); 16 20 const profile = await getProfileByDid(did).catch(() => null); 17 - const bannerCid = profile?.bannerCid; 18 - if (!bannerCid) { 21 + if (!profile?.bannerCid) { 19 22 return new Response("not found", { status: 404 }); 20 23 } 21 - return new Response(null, { 22 - status: 302, 23 - headers: { 24 - location: bskyCdnBannerUrl(did, bannerCid), 25 - "cache-control": 26 - "public, max-age=300, s-maxage=3600, stale-while-revalidate=3600", 27 - }, 28 - }); 24 + try { 25 + const upstream = await fetchBlobPublic( 26 + profile.pdsUrl, 27 + did, 28 + profile.bannerCid, 29 + ); 30 + if (!upstream.ok) { 31 + return new Response("not found", { status: 404 }); 32 + } 33 + const headers = new Headers(); 34 + headers.set( 35 + "content-type", 36 + upstream.headers.get("content-type") ?? 37 + profile.bannerMime ?? 38 + "image/jpeg", 39 + ); 40 + headers.set( 41 + "cache-control", 42 + "public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400", 43 + ); 44 + headers.set("etag", profile.bannerCid); 45 + return new Response(upstream.body, { status: 200, headers }); 46 + } catch (err) { 47 + console.warn("[banner] proxy error:", err); 48 + return new Response("upstream error", { status: 502 }); 49 + } 29 50 }), 30 51 });
+6 -4
routes/explore/[handle].tsx
··· 28 28 import { accountProviderName } from "../../lib/account-providers.ts"; 29 29 import { buildAccountMenuProps } from "../../lib/account-menu-props.ts"; 30 30 import { getAppUser } from "../../lib/account-types.ts"; 31 - import { bskyCdnAvatarUrl, bskyCdnBannerUrl } from "../../lib/avatar.ts"; 31 + import { bskyCdnAvatarUrl } from "../../lib/avatar.ts"; 32 32 import { 33 33 listProfileUpdates, 34 34 type ProfileUpdateRow, ··· 85 85 const pageDescription = profile.description || 86 86 messages.detail.missingProfile; 87 87 const ogImageUrl = profile.bannerCid 88 - ? bskyCdnBannerUrl(profile.did, profile.bannerCid) 88 + ? new URL( 89 + `/api/registry/banner/${encodeURIComponent(profile.did)}`, 90 + ctx.url.origin, 91 + ).toString() 89 92 : undefined; 90 93 ctx.state.pageMeta = { 91 94 title: pageTitle, ··· 174 177 * brand name (Bluesky, etc.) and fall back to the bare host. */ 175 178 const providerName = accountProviderName(profile.pdsUrl); 176 179 const bannerUrl = profile.bannerCid 177 - ? bskyCdnBannerUrl(profile.did, profile.bannerCid) 180 + ? `/api/registry/banner/${encodeURIComponent(profile.did)}` 178 181 : null; 179 182 const shareCopy = t.detail.share; 180 183 return ( ··· 190 193 <ShareButton 191 194 url={shareUrl} 192 195 title={shareCopy.shareTitle(profile.name)} 193 - text={profile.description} 194 196 copy={{ 195 197 button: shareCopy.button, 196 198 copyLink: shareCopy.copyLink,