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.
···11import type { ProfileRow } from "../../lib/registry.ts";
22import { PUBLIC_CATEGORIES } from "../../lib/lexicons.ts";
33import { useT } from "../../i18n/mod.ts";
44+import { bskyCdnAvatarUrl } from "../../lib/avatar.ts";
45import VerifiedBadge from "../VerifiedBadge.tsx";
5667interface Props {
···3334 {profile.avatarCid
3435 ? (
3536 <img
3636- src={`/api/registry/avatar/${encodeURIComponent(profile.did)}`}
3737+ src={bskyCdnAvatarUrl(profile.did, profile.avatarCid)}
3738 alt=""
3839 loading="lazy"
3940 decoding="async"
+2-1
components/explore/ProfileHero.tsx
···11import type { ProfileRow } from "../../lib/registry.ts";
22import { PUBLIC_CATEGORIES } from "../../lib/lexicons.ts";
33+import { bskyCdnAvatarUrl } from "../../lib/avatar.ts";
34import {
45 type ResolvedIconKind,
56 resolveLink,
···6768 {profile.avatarCid
6869 ? (
6970 <img
7070- src={`/api/registry/avatar/${encodeURIComponent(profile.did)}`}
7171+ src={bskyCdnAvatarUrl(profile.did, profile.avatarCid)}
7172 alt={profile.name}
7273 decoding="async"
7374 />
+3-4
islands/CreateProfileForm.tsx
···1111 getAtmosphereService,
1212 visibleAtmosphereServices,
1313} from "../lib/atmosphere-links.ts";
1414+import { bskyCdnAvatarUrl } from "../lib/avatar.ts";
1415import { BSKY_CLIENTS, getBskyClient } from "../lib/bsky-clients.ts";
1516import { useT } from "../i18n/mod.ts";
1617import BskyClientPickerModal from "./BskyClientPickerModal.tsx";
···292293 * check this first because in the prefill case `initial.avatar`
293294 * is also set (so it can carry through the BlobRef on Save) but
294295 * the registry-side proxy doesn't have anything to serve yet.
295295- * 3. Existing registry record → cached server proxy.
296296+ * 3. Existing registry record → Bluesky CDN avatar by did/cid.
296297 * 4. Empty placeholder.
297298 */
298299 const avatarPreview = useSignal<string | null>(
299300 initialAvatarUrl ??
300300- (initial?.avatar
301301- ? `/api/registry/avatar/${encodeURIComponent(did)}`
302302- : null),
301301+ (initial?.avatar ? bskyCdnAvatarUrl(did, initial.avatar.ref) : null),
303302 );
304303 const avatarFile = useSignal<File | null>(null);
305304 const avatarRemoved = useSignal(false);
+8
lib/avatar.ts
···11+/**
22+ * Bluesky's public CDN is a cached proxy for repo blob avatars. Any profile
33+ * avatar stored as a did/cid pair can use this directly and avoid our app
44+ * server's PDS blob proxy on hot UI paths.
55+ */
66+export function bskyCdnAvatarUrl(did: string, cid: string): string {
77+ return `https://cdn.bsky.app/img/avatar/plain/${did}/${cid}`;
88+}
+3-2
lib/public-profile.ts
···1212 */
1313import type { LinkEntry, ScreenshotEntry } from "./lexicons.ts";
1414import type { ProfileRow } from "./registry.ts";
1515+import { bskyCdnAvatarUrl } from "./avatar.ts";
15161617export interface PublicProfileJson {
1718 did: string;
···2930 screenshotUrls: string[];
3031 avatarCid: string | null;
3132 avatarMime: string | null;
3232- /** Fully-qualified URL for the profile avatar image proxy, or null. */
3333+ /** Fully-qualified URL for the profile avatar image, or null. */
3334 avatarUrl: string | null;
3435 /**
3536 * True when the project shows the public verified badge on Explore
···5556 origin: string,
5657): PublicProfileJson {
5758 const avatarUrl = profile.avatarCid
5858- ? `${origin}/api/registry/avatar/${encodeURIComponent(profile.did)}`
5959+ ? bskyCdnAvatarUrl(profile.did, profile.avatarCid)
5960 : null;
6061 const verified = profile.iconAccessStatus === "granted";
6162 const iconUrl = profile.iconCid &&
+1-4
routes/account/reviews.tsx
···99 getAppUser,
1010 getEffectiveAccountType,
1111} from "../../lib/account-types.ts";
1212+import { bskyCdnAvatarUrl } from "../../lib/avatar.ts";
1213import { getProfileByDid } from "../../lib/registry.ts";
1314import { listReviewsByReviewer, type ReviewRow } from "../../lib/reviews.ts";
1414-1515-function bskyCdnAvatarUrl(did: string, cid: string): string {
1616- return `https://cdn.bsky.app/img/avatar/plain/${did}/${cid}`;
1717-}
18151916interface ReviewWithTarget extends ReviewRow {
2017 targetHandle: string;
+18-36
routes/api/me/avatar.ts
···22 * Avatar for the currently signed-in user, used by the explore-page
33 * AccountMenu. Resolution order:
44 *
55- * 1. Registry profile avatar (proxied through /api/registry/avatar/:did,
66- * so we benefit from its cache headers + ETag).
77- * 2. Bluesky `app.bsky.actor.profile` avatar fetched from the user's
88- * PDS via getBlob — covers the case where the user has signed in
99- * but hasn't published a registry profile yet.
55+ * 1. Registry profile avatar redirected to the Bluesky CDN.
66+ * 2. Bluesky `app.bsky.actor.profile` avatar redirected to the same CDN —
77+ * covers the case where the user has signed in but hasn't published a
88+ * registry profile yet.
109 * 3. 404 — the AccountMenu falls back to a handle-initial avatar.
1110 *
1211 * No request body, no params: identity comes from the session cookie via
···1615import { define } from "../../../utils.ts";
1716import { getProfileByDid } from "../../../lib/registry.ts";
1817import { loadSession } from "../../../lib/oauth.ts";
1919-import { fetchBlobPublic, getBskyProfile } from "../../../lib/pds.ts";
1818+import { bskyCdnAvatarUrl } from "../../../lib/avatar.ts";
1919+import { getBskyProfile } from "../../../lib/pds.ts";
20202121const NOT_FOUND = new Response("not found", { status: 404 });
2222···2525 const user = ctx.state.user;
2626 if (!user) return NOT_FOUND;
27272828- /** Prefer the registry-cached avatar — it's already proxied with
2929- * long cache headers and an ETag, and works even if the user later
3030- * signs out (still public). Internal redirect (303) keeps this
3131- * route cheap; the browser follows once and then caches the
3232- * resolved URL. */
2828+ /** Prefer the registry avatar. This route stays per-session for cache
2929+ * busting, but the image bytes come from Bluesky's CDN. */
3330 const profile = await getProfileByDid(user.did).catch(() => null);
3431 if (profile?.avatarCid) {
3532 return new Response(null, {
3633 status: 302,
3734 headers: {
3838- location: `/api/registry/avatar/${encodeURIComponent(user.did)}`,
3535+ location: bskyCdnAvatarUrl(user.did, profile.avatarCid),
3936 "cache-control": "private, max-age=300, stale-while-revalidate=86400",
4037 },
4138 });
4239 }
43404444- /** No registry profile yet — fall back to the user's Bluesky avatar
4545- * on their PDS, so the menu still shows something familiar after
4646- * their first sign-in. We stream the bytes directly because the
4747- * PDS getBlob URL isn't cacheable on its own (it's pinned to the
4848- * CID though, so once we've seen it we can cache it here). */
4141+ /** No registry profile yet — fall back to the user's Bluesky avatar so
4242+ * the menu still shows something familiar after their first sign-in. */
4943 const session = await loadSession(user.did).catch(() => null);
5044 if (!session) return NOT_FOUND;
5145 const bsky = await getBskyProfile(session.pdsUrl, user.did).catch(() =>
···5347 );
5448 const cid = bsky?.avatar?.ref.$link;
5549 if (!bsky || !cid) return NOT_FOUND;
5656-5757- try {
5858- const upstream = await fetchBlobPublic(session.pdsUrl, user.did, cid);
5959- if (!upstream.ok) return NOT_FOUND;
6060- const headers = new Headers();
6161- headers.set(
6262- "content-type",
6363- upstream.headers.get("content-type") ?? bsky.avatar?.mimeType ??
6464- "application/octet-stream",
6565- );
6666- headers.set(
6767- "cache-control",
6868- "private, max-age=600, stale-while-revalidate=86400",
6969- );
7070- headers.set("etag", cid);
7171- return new Response(upstream.body, { status: 200, headers });
7272- } catch {
7373- return NOT_FOUND;
7474- }
5050+ return new Response(null, {
5151+ status: 302,
5252+ headers: {
5353+ location: bskyCdnAvatarUrl(user.did, cid),
5454+ "cache-control": "private, max-age=600, stale-while-revalidate=86400",
5555+ },
5656+ });
7557 },
7658});
+11-31
routes/api/registry/avatar/[did].ts
···11/**
22- * Proxy + cache the avatar blob for a registry profile. We look up the
33- * (pdsUrl, avatar_cid) pair from our own DB, then stream the bytes back
44- * with long cache headers. Falls back to 404 if no avatar is set.
22+ * Compatibility endpoint for registry profile avatars. Primary UI paths use
33+ * the Bluesky CDN directly; this route redirects existing consumers there.
54 */
65import { define } from "../../../../utils.ts";
66+import { bskyCdnAvatarUrl } from "../../../../lib/avatar.ts";
77import { getProfileByDid } from "../../../../lib/registry.ts";
88-import { fetchBlobPublic } from "../../../../lib/pds.ts";
98import { withRateLimit } from "../../../../lib/rate-limit.ts";
1010-1111-function bskyCdnAvatarUrl(did: string, cid: string): string {
1212- return `https://cdn.bsky.app/img/avatar/plain/${did}/${cid}`;
1313-}
1491510export const handler = define.handlers({
1611 GET: withRateLimit(async (ctx) => {
···1914 if (!profile || !profile.avatarCid) {
2015 return new Response("not found", { status: 404 });
2116 }
2222- try {
2323- const upstream = await fetchBlobPublic(
2424- profile.pdsUrl,
2525- did,
2626- profile.avatarCid,
2727- );
2828- if (!upstream.ok) {
2929- return Response.redirect(bskyCdnAvatarUrl(did, profile.avatarCid), 302);
3030- }
3131- const headers = new Headers();
3232- const ct = upstream.headers.get("content-type") ?? profile.avatarMime ??
3333- "application/octet-stream";
3434- headers.set("content-type", ct);
3535- headers.set(
3636- "cache-control",
3737- "public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400",
3838- );
3939- headers.set("etag", profile.avatarCid);
4040- return new Response(upstream.body, { status: 200, headers });
4141- } catch (err) {
4242- console.warn("avatar proxy error:", err);
4343- return Response.redirect(bskyCdnAvatarUrl(did, profile.avatarCid), 302);
4444- }
1717+ return new Response(null, {
1818+ status: 302,
1919+ headers: {
2020+ location: bskyCdnAvatarUrl(did, profile.avatarCid),
2121+ "cache-control":
2222+ "public, max-age=300, s-maxage=3600, stale-while-revalidate=3600",
2323+ },
2424+ });
4525 }),
4626});
+2
routes/api/registry/screenshot/[did]/[index].ts
···3535 );
3636 headers.set(
3737 "cache-control",
3838+ // The blob CID is immutable, but this route is keyed by did/index, so
3939+ // keep shared caching bounded in case a profile replaces a screenshot.
3840 "public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400",
3941 );
4042 headers.set("etag", cid);
···1010import { buildAccountMenuProps } from "../../lib/account-menu-props.ts";
1111import { getEffectiveAccountType } from "../../lib/account-types.ts";
1212import { listProfileUpdates } from "../../lib/profile-updates.ts";
1313-1414-/**
1515- * Build the deterministic public Bluesky CDN URL for a user's avatar
1616- * blob. The CDN is a thin cached proxy in front of the user's PDS, so
1717- * any did/cid pair from `app.bsky.actor.profile` resolves cleanly here
1818- * with cache headers + the correct content-type. Using this URL avoids
1919- * routing the prefill avatar through our own server (which adds a hop
2020- * and can fail in subtle ways on some PDS hosts).
2121- */
2222-function bskyCdnAvatarUrl(did: string, cid: string): string {
2323- return `https://cdn.bsky.app/img/avatar/plain/${did}/${cid}`;
2424-}
1313+import { bskyCdnAvatarUrl } from "../../lib/avatar.ts";
25142615export const handler = define.handlers({
2716 async GET(ctx) {
···4938 const t = getMessages(ctx.state.locale);
50395140 let initial: Parameters<typeof CreateProfileForm>[0]["initial"] = null;
5252- /** When showing a Bluesky-prefilled draft (no registry record yet), we
5353- * display the user's PDS-hosted avatar directly via getBlob. After the
5454- * registry record exists, the form switches to the cached
5555- * /api/registry/avatar/:did proxy. */
4141+ /** When showing a Bluesky-prefilled draft (no registry record yet), and
4242+ * after a registry record exists, the form previews the avatar through
4343+ * Bluesky's CDN whenever a did/cid pair is available. */
5644 let initialAvatarUrl: string | null = null;
5745 /** Owner-aware lookup: include taken-down rows so the form can
5846 * surface a "Your profile has been taken down" banner with the
+1-4
routes/users/[handle].tsx
···44import { getMessages } from "../../i18n/mod.ts";
55import { buildAccountMenuProps } from "../../lib/account-menu-props.ts";
66import { getAppUserByHandle } from "../../lib/account-types.ts";
77+import { bskyCdnAvatarUrl } from "../../lib/avatar.ts";
78import { getBskyClient } from "../../lib/bsky-clients.ts";
89import { getProfileByHandle } from "../../lib/registry.ts";
99-1010-function bskyCdnAvatarUrl(did: string, cid: string): string {
1111- return `https://cdn.bsky.app/img/avatar/plain/${did}/${cid}`;
1212-}
13101411export const handler = define.handlers({
1512 async GET(ctx) {