* Fetches Bluesky avatars for a set of authors at build time. * Returns a map of handle → avatar URL.
( imagesDir: string, handles: string[], )
| 24 | * Returns a map of handle → avatar URL. |
| 25 | */ |
| 26 | async function fetchBlueskyAvatars( |
| 27 | imagesDir: string, |
| 28 | handles: string[], |
| 29 | ): Promise<Map<string, string>> { |
| 30 | const avatarMap = new Map<string, string>() |
| 31 | if (handles.length === 0) return avatarMap |
| 32 | |
| 33 | try { |
| 34 | const params = new URLSearchParams() |
| 35 | for (const handle of handles) { |
| 36 | params.append('actors', handle) |
| 37 | } |
| 38 | |
| 39 | const response = await fetch( |
| 40 | `${BLUESKY_API}/xrpc/app.bsky.actor.getProfiles?${params.toString()}`, |
| 41 | ) |
| 42 | |
| 43 | if (!response.ok) { |
| 44 | console.warn(`[blog] Failed to fetch Bluesky profiles: ${response.status}`) |
| 45 | return avatarMap |
| 46 | } |
| 47 | |
| 48 | const data = (await response.json()) as { profiles: Array<{ handle: string; avatar?: string }> } |
| 49 | |
| 50 | for (const profile of data.profiles) { |
| 51 | if (profile.avatar) { |
| 52 | const hash = crypto.createHash('sha256').update(profile.avatar).digest('hex') |
| 53 | const dest = join(imagesDir, `${hash}.png`) |
| 54 | |
| 55 | if (!existsSync(dest)) { |
| 56 | const res = await fetch(`${profile.avatar}@png`) |
| 57 | if (!res.ok || !res.body) { |
| 58 | console.warn(`[blog] Failed to fetch Bluesky avatar: ${profile.avatar}@png`) |
| 59 | continue |
| 60 | } |
| 61 | await writeFile(join(imagesDir, `${hash}.png`), res.body) |
| 62 | } |
| 63 | |
| 64 | avatarMap.set(profile.handle, `/blog/avatar/${hash}.png`) |
| 65 | } |
| 66 | } |
| 67 | } catch (error) { |
| 68 | console.warn(`[blog] Failed to fetch Bluesky avatars:`, error) |
| 69 | } |
| 70 | |
| 71 | return avatarMap |
| 72 | } |
| 73 | |
| 74 | /** |
| 75 | * Resolves authors with their Bluesky avatars and profile URLs. |