this repo has no description
10
fork

Configure Feed

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

perf(registry): serve avatars from Bluesky CDN

Use direct did/cid CDN URLs for primary avatar paths and keep the app avatar endpoint as a compatibility redirect to avoid proxying image bytes through the app.

Made-with: Cursor

+57 -104
+2 -1
components/explore/ProfileCard.tsx
··· 1 1 import type { ProfileRow } from "../../lib/registry.ts"; 2 2 import { PUBLIC_CATEGORIES } from "../../lib/lexicons.ts"; 3 3 import { useT } from "../../i18n/mod.ts"; 4 + import { bskyCdnAvatarUrl } from "../../lib/avatar.ts"; 4 5 import VerifiedBadge from "../VerifiedBadge.tsx"; 5 6 6 7 interface Props { ··· 33 34 {profile.avatarCid 34 35 ? ( 35 36 <img 36 - src={`/api/registry/avatar/${encodeURIComponent(profile.did)}`} 37 + src={bskyCdnAvatarUrl(profile.did, profile.avatarCid)} 37 38 alt="" 38 39 loading="lazy" 39 40 decoding="async"
+2 -1
components/explore/ProfileHero.tsx
··· 1 1 import type { ProfileRow } from "../../lib/registry.ts"; 2 2 import { PUBLIC_CATEGORIES } from "../../lib/lexicons.ts"; 3 + import { bskyCdnAvatarUrl } from "../../lib/avatar.ts"; 3 4 import { 4 5 type ResolvedIconKind, 5 6 resolveLink, ··· 67 68 {profile.avatarCid 68 69 ? ( 69 70 <img 70 - src={`/api/registry/avatar/${encodeURIComponent(profile.did)}`} 71 + src={bskyCdnAvatarUrl(profile.did, profile.avatarCid)} 71 72 alt={profile.name} 72 73 decoding="async" 73 74 />
+3 -4
islands/CreateProfileForm.tsx
··· 11 11 getAtmosphereService, 12 12 visibleAtmosphereServices, 13 13 } from "../lib/atmosphere-links.ts"; 14 + import { bskyCdnAvatarUrl } from "../lib/avatar.ts"; 14 15 import { BSKY_CLIENTS, getBskyClient } from "../lib/bsky-clients.ts"; 15 16 import { useT } from "../i18n/mod.ts"; 16 17 import BskyClientPickerModal from "./BskyClientPickerModal.tsx"; ··· 292 293 * check this first because in the prefill case `initial.avatar` 293 294 * is also set (so it can carry through the BlobRef on Save) but 294 295 * the registry-side proxy doesn't have anything to serve yet. 295 - * 3. Existing registry record → cached server proxy. 296 + * 3. Existing registry record → Bluesky CDN avatar by did/cid. 296 297 * 4. Empty placeholder. 297 298 */ 298 299 const avatarPreview = useSignal<string | null>( 299 300 initialAvatarUrl ?? 300 - (initial?.avatar 301 - ? `/api/registry/avatar/${encodeURIComponent(did)}` 302 - : null), 301 + (initial?.avatar ? bskyCdnAvatarUrl(did, initial.avatar.ref) : null), 303 302 ); 304 303 const avatarFile = useSignal<File | null>(null); 305 304 const avatarRemoved = useSignal(false);
+8
lib/avatar.ts
··· 1 + /** 2 + * Bluesky's public CDN is a cached proxy for repo blob avatars. Any profile 3 + * avatar stored as a did/cid pair can use this directly and avoid our app 4 + * server's PDS blob proxy on hot UI paths. 5 + */ 6 + export function bskyCdnAvatarUrl(did: string, cid: string): string { 7 + return `https://cdn.bsky.app/img/avatar/plain/${did}/${cid}`; 8 + }
+3 -2
lib/public-profile.ts
··· 12 12 */ 13 13 import type { LinkEntry, ScreenshotEntry } from "./lexicons.ts"; 14 14 import type { ProfileRow } from "./registry.ts"; 15 + import { bskyCdnAvatarUrl } from "./avatar.ts"; 15 16 16 17 export interface PublicProfileJson { 17 18 did: string; ··· 29 30 screenshotUrls: string[]; 30 31 avatarCid: string | null; 31 32 avatarMime: string | null; 32 - /** Fully-qualified URL for the profile avatar image proxy, or null. */ 33 + /** Fully-qualified URL for the profile avatar image, or null. */ 33 34 avatarUrl: string | null; 34 35 /** 35 36 * True when the project shows the public verified badge on Explore ··· 55 56 origin: string, 56 57 ): PublicProfileJson { 57 58 const avatarUrl = profile.avatarCid 58 - ? `${origin}/api/registry/avatar/${encodeURIComponent(profile.did)}` 59 + ? bskyCdnAvatarUrl(profile.did, profile.avatarCid) 59 60 : null; 60 61 const verified = profile.iconAccessStatus === "granted"; 61 62 const iconUrl = profile.iconCid &&
+1 -4
routes/account/reviews.tsx
··· 9 9 getAppUser, 10 10 getEffectiveAccountType, 11 11 } from "../../lib/account-types.ts"; 12 + import { bskyCdnAvatarUrl } from "../../lib/avatar.ts"; 12 13 import { getProfileByDid } from "../../lib/registry.ts"; 13 14 import { listReviewsByReviewer, type ReviewRow } from "../../lib/reviews.ts"; 14 - 15 - function bskyCdnAvatarUrl(did: string, cid: string): string { 16 - return `https://cdn.bsky.app/img/avatar/plain/${did}/${cid}`; 17 - } 18 15 19 16 interface ReviewWithTarget extends ReviewRow { 20 17 targetHandle: string;
+18 -36
routes/api/me/avatar.ts
··· 2 2 * Avatar for the currently signed-in user, used by the explore-page 3 3 * AccountMenu. Resolution order: 4 4 * 5 - * 1. Registry profile avatar (proxied through /api/registry/avatar/:did, 6 - * so we benefit from its cache headers + ETag). 7 - * 2. Bluesky `app.bsky.actor.profile` avatar fetched from the user's 8 - * PDS via getBlob — covers the case where the user has signed in 9 - * but hasn't published a registry profile yet. 5 + * 1. Registry profile avatar redirected to the Bluesky CDN. 6 + * 2. Bluesky `app.bsky.actor.profile` avatar redirected to the same CDN — 7 + * covers the case where the user has signed in but hasn't published a 8 + * registry profile yet. 10 9 * 3. 404 — the AccountMenu falls back to a handle-initial avatar. 11 10 * 12 11 * No request body, no params: identity comes from the session cookie via ··· 16 15 import { define } from "../../../utils.ts"; 17 16 import { getProfileByDid } from "../../../lib/registry.ts"; 18 17 import { loadSession } from "../../../lib/oauth.ts"; 19 - import { fetchBlobPublic, getBskyProfile } from "../../../lib/pds.ts"; 18 + import { bskyCdnAvatarUrl } from "../../../lib/avatar.ts"; 19 + import { getBskyProfile } from "../../../lib/pds.ts"; 20 20 21 21 const NOT_FOUND = new Response("not found", { status: 404 }); 22 22 ··· 25 25 const user = ctx.state.user; 26 26 if (!user) return NOT_FOUND; 27 27 28 - /** Prefer the registry-cached avatar — it's already proxied with 29 - * long cache headers and an ETag, and works even if the user later 30 - * signs out (still public). Internal redirect (303) keeps this 31 - * route cheap; the browser follows once and then caches the 32 - * resolved URL. */ 28 + /** Prefer the registry avatar. This route stays per-session for cache 29 + * busting, but the image bytes come from Bluesky's CDN. */ 33 30 const profile = await getProfileByDid(user.did).catch(() => null); 34 31 if (profile?.avatarCid) { 35 32 return new Response(null, { 36 33 status: 302, 37 34 headers: { 38 - location: `/api/registry/avatar/${encodeURIComponent(user.did)}`, 35 + location: bskyCdnAvatarUrl(user.did, profile.avatarCid), 39 36 "cache-control": "private, max-age=300, stale-while-revalidate=86400", 40 37 }, 41 38 }); 42 39 } 43 40 44 - /** No registry profile yet — fall back to the user's Bluesky avatar 45 - * on their PDS, so the menu still shows something familiar after 46 - * their first sign-in. We stream the bytes directly because the 47 - * PDS getBlob URL isn't cacheable on its own (it's pinned to the 48 - * CID though, so once we've seen it we can cache it here). */ 41 + /** No registry profile yet — fall back to the user's Bluesky avatar so 42 + * the menu still shows something familiar after their first sign-in. */ 49 43 const session = await loadSession(user.did).catch(() => null); 50 44 if (!session) return NOT_FOUND; 51 45 const bsky = await getBskyProfile(session.pdsUrl, user.did).catch(() => ··· 53 47 ); 54 48 const cid = bsky?.avatar?.ref.$link; 55 49 if (!bsky || !cid) return NOT_FOUND; 56 - 57 - try { 58 - const upstream = await fetchBlobPublic(session.pdsUrl, user.did, cid); 59 - if (!upstream.ok) return NOT_FOUND; 60 - const headers = new Headers(); 61 - headers.set( 62 - "content-type", 63 - upstream.headers.get("content-type") ?? bsky.avatar?.mimeType ?? 64 - "application/octet-stream", 65 - ); 66 - headers.set( 67 - "cache-control", 68 - "private, max-age=600, stale-while-revalidate=86400", 69 - ); 70 - headers.set("etag", cid); 71 - return new Response(upstream.body, { status: 200, headers }); 72 - } catch { 73 - return NOT_FOUND; 74 - } 50 + return new Response(null, { 51 + status: 302, 52 + headers: { 53 + location: bskyCdnAvatarUrl(user.did, cid), 54 + "cache-control": "private, max-age=600, stale-while-revalidate=86400", 55 + }, 56 + }); 75 57 }, 76 58 });
+11 -31
routes/api/registry/avatar/[did].ts
··· 1 1 /** 2 - * Proxy + cache the avatar blob for a registry profile. We look up the 3 - * (pdsUrl, avatar_cid) pair from our own DB, then stream the bytes back 4 - * with long cache headers. Falls back to 404 if no avatar is set. 2 + * Compatibility endpoint for registry profile avatars. Primary UI paths use 3 + * the Bluesky CDN directly; this route redirects existing consumers there. 5 4 */ 6 5 import { define } from "../../../../utils.ts"; 6 + import { bskyCdnAvatarUrl } from "../../../../lib/avatar.ts"; 7 7 import { getProfileByDid } from "../../../../lib/registry.ts"; 8 - import { fetchBlobPublic } from "../../../../lib/pds.ts"; 9 8 import { withRateLimit } from "../../../../lib/rate-limit.ts"; 10 - 11 - function bskyCdnAvatarUrl(did: string, cid: string): string { 12 - return `https://cdn.bsky.app/img/avatar/plain/${did}/${cid}`; 13 - } 14 9 15 10 export const handler = define.handlers({ 16 11 GET: withRateLimit(async (ctx) => { ··· 19 14 if (!profile || !profile.avatarCid) { 20 15 return new Response("not found", { status: 404 }); 21 16 } 22 - try { 23 - const upstream = await fetchBlobPublic( 24 - profile.pdsUrl, 25 - did, 26 - profile.avatarCid, 27 - ); 28 - if (!upstream.ok) { 29 - return Response.redirect(bskyCdnAvatarUrl(did, profile.avatarCid), 302); 30 - } 31 - const headers = new Headers(); 32 - const ct = upstream.headers.get("content-type") ?? profile.avatarMime ?? 33 - "application/octet-stream"; 34 - headers.set("content-type", ct); 35 - headers.set( 36 - "cache-control", 37 - "public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400", 38 - ); 39 - headers.set("etag", profile.avatarCid); 40 - return new Response(upstream.body, { status: 200, headers }); 41 - } catch (err) { 42 - console.warn("avatar proxy error:", err); 43 - return Response.redirect(bskyCdnAvatarUrl(did, profile.avatarCid), 302); 44 - } 17 + return new Response(null, { 18 + status: 302, 19 + headers: { 20 + location: bskyCdnAvatarUrl(did, profile.avatarCid), 21 + "cache-control": 22 + "public, max-age=300, s-maxage=3600, stale-while-revalidate=3600", 23 + }, 24 + }); 45 25 }), 46 26 });
+2
routes/api/registry/screenshot/[did]/[index].ts
··· 35 35 ); 36 36 headers.set( 37 37 "cache-control", 38 + // The blob CID is immutable, but this route is keyed by did/index, so 39 + // keep shared caching bounded in case a profile replaces a screenshot. 38 40 "public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400", 39 41 ); 40 42 headers.set("etag", cid);
+2 -5
routes/explore/[handle].tsx
··· 27 27 import { accountProviderName } from "../../lib/account-providers.ts"; 28 28 import { buildAccountMenuProps } from "../../lib/account-menu-props.ts"; 29 29 import { getAppUser } from "../../lib/account-types.ts"; 30 + import { bskyCdnAvatarUrl } from "../../lib/avatar.ts"; 30 31 import { 31 32 listProfileUpdates, 32 33 type ProfileUpdateRow, 33 34 } from "../../lib/profile-updates.ts"; 34 - 35 - function bskyCdnAvatarUrl(did: string, cid: string): string { 36 - return `https://cdn.bsky.app/img/avatar/plain/${did}/${cid}`; 37 - } 38 35 39 36 export const handler = define.handlers({ 40 37 async GET(ctx) { ··· 277 274 const reviewerAvatarUrl = appUser?.avatarCid && appUser.avatarMime 278 275 ? bskyCdnAvatarUrl(review.reviewerDid, appUser.avatarCid) 279 276 : profile?.avatarCid 280 - ? `/api/registry/avatar/${encodeURIComponent(review.reviewerDid)}` 277 + ? bskyCdnAvatarUrl(review.reviewerDid, profile.avatarCid) 281 278 : null; 282 279 return { 283 280 ...review,
+4 -16
routes/explore/manage.tsx
··· 10 10 import { buildAccountMenuProps } from "../../lib/account-menu-props.ts"; 11 11 import { getEffectiveAccountType } from "../../lib/account-types.ts"; 12 12 import { listProfileUpdates } from "../../lib/profile-updates.ts"; 13 - 14 - /** 15 - * Build the deterministic public Bluesky CDN URL for a user's avatar 16 - * blob. The CDN is a thin cached proxy in front of the user's PDS, so 17 - * any did/cid pair from `app.bsky.actor.profile` resolves cleanly here 18 - * with cache headers + the correct content-type. Using this URL avoids 19 - * routing the prefill avatar through our own server (which adds a hop 20 - * and can fail in subtle ways on some PDS hosts). 21 - */ 22 - function bskyCdnAvatarUrl(did: string, cid: string): string { 23 - return `https://cdn.bsky.app/img/avatar/plain/${did}/${cid}`; 24 - } 13 + import { bskyCdnAvatarUrl } from "../../lib/avatar.ts"; 25 14 26 15 export const handler = define.handlers({ 27 16 async GET(ctx) { ··· 49 38 const t = getMessages(ctx.state.locale); 50 39 51 40 let initial: Parameters<typeof CreateProfileForm>[0]["initial"] = null; 52 - /** When showing a Bluesky-prefilled draft (no registry record yet), we 53 - * display the user's PDS-hosted avatar directly via getBlob. After the 54 - * registry record exists, the form switches to the cached 55 - * /api/registry/avatar/:did proxy. */ 41 + /** When showing a Bluesky-prefilled draft (no registry record yet), and 42 + * after a registry record exists, the form previews the avatar through 43 + * Bluesky's CDN whenever a did/cid pair is available. */ 56 44 let initialAvatarUrl: string | null = null; 57 45 /** Owner-aware lookup: include taken-down rows so the form can 58 46 * surface a "Your profile has been taken down" banner with the
+1 -4
routes/users/[handle].tsx
··· 4 4 import { getMessages } from "../../i18n/mod.ts"; 5 5 import { buildAccountMenuProps } from "../../lib/account-menu-props.ts"; 6 6 import { getAppUserByHandle } from "../../lib/account-types.ts"; 7 + import { bskyCdnAvatarUrl } from "../../lib/avatar.ts"; 7 8 import { getBskyClient } from "../../lib/bsky-clients.ts"; 8 9 import { getProfileByHandle } from "../../lib/registry.ts"; 9 - 10 - function bskyCdnAvatarUrl(did: string, cid: string): string { 11 - return `https://cdn.bsky.app/img/avatar/plain/${did}/${cid}`; 12 - } 13 10 14 11 export const handler = define.handlers({ 15 12 async GET(ctx) {