Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/components/DemoStage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const liveUrl = computed(() => live && !demo.video ? demo.liveUrl : null)
:src="liveUrl"
sandbox="allow-scripts"
:title="`Live output of /${demo.name}`"
loading="lazy"
class="demo-stage__frame"
/>
<DemoMedia v-else :demo :eager play="visible" />
Expand Down
5 changes: 4 additions & 1 deletion app/components/WeeklyEmailPreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import type { DigestDemoResponse } from '#layers/identity/server/api/digest/demo

// A failed demo costs the section its picture, never its copy or its CTA:
// `data` stays null on error and `hasDemo` gates the whole frame.
const { data: demo } = await useFetch<DigestDemoResponse>('/api/digest/demo', {
// Only the card and its row count reach the payload. The answer also carries
// the whole email as `html`, which this preview never renders.
const { data: demo } = await useFetch('/api/digest/demo', {
key: 'home-digest-demo-v1',
transform: ({ card, rowCount }: DigestDemoResponse) => ({ card, rowCount }),
})

const hasDemo = computed(() => !!demo.value?.card && (demo.value?.rowCount ?? 0) > 0)
Expand Down
11 changes: 4 additions & 7 deletions app/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,10 @@ const [
query: { limit: 24 },
// The board ships only its head. The rest of the named Skills, with their
// posts and star series, serve `/skills/trending` and would ride in this
// page's payload unread.
// page's payload unread. The texture reads only Skill names, so the
// repository cards stay out too: they were 47 kB of the payload.
transform: (feed: TrendingFeedResponse) => ({
items: feed.items,
textureNames: feed.items.flatMap(repo => repo.skills.map(skill => `${repo.owner}/${repo.repo}/${skill.name}`)),
board: feedBoardRows(feed).slice(0, HOME_BOARD_ROWS),
}),
})),
Expand All @@ -110,8 +111,6 @@ const { data: demosData } = useLazyFetch<{ items: HomeDemoItem[] }>('/api/skill-
const recentUpdates = computed(() => updatesData.value?.items ?? [])
const recentPublishes = computed(() => publishesData.value?.items ?? [])

const trendingRepos = computed(() => trendingData.value?.items ?? [])

/** Owners whose avatar failed, which means the GitHub account is gone; see `/skills/trending`. */
const missingAvatars = ref(new Set<string>())
function onAvatarError(owner: string) {
Expand All @@ -132,9 +131,7 @@ const showTrending = computed(() => (trendingData.value?.board.length ?? 0) >= H
const weekBoardPath = trendingRangeMeta('week').path

/** Real Skill names from this week's trending repositories, for the hero texture. */
const heroTextureNames = computed(() =>
trendingRepos.value.flatMap(repo => repo.skills.map(skill => `${repo.owner}/${repo.repo}/${skill.name}`)),
)
const heroTextureNames = computed(() => trendingData.value?.textureNames ?? [])

/**
* Track order, measured 2026-09-04 rather than assumed.
Expand Down
15 changes: 14 additions & 1 deletion shared/image-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,20 @@ export function avatarProxyUrl(src: string | null | undefined): string | undefin
const target = parseImageTarget(src)
if (target._tag === 'Err' || !isAvatarUrl(target.url))
return undefined
return `${IMAGE_PROXY_PREFIX}/avatar?url=${encodeURIComponent(target.url.href)}`
return `${IMAGE_PROXY_PREFIX}/avatar?url=${encodeURIComponent(avatarAtThumbnailSize(target.url).href)}`
}

/**
* Bluesky's `avatar` path serves the upload at full size, often 1000 pixels
* square. Pages draw avatars at 20 to 40 pixels, so the trending board shipped
* about 760 kB of Bluesky avatars where the `avatar_thumbnail` path sends 39 kB.
*/
function avatarAtThumbnailSize(url: URL): URL {
if (url.hostname !== 'cdn.bsky.app' || !url.pathname.startsWith('/img/avatar/'))
return url
const thumbnail = new URL(url)
thumbnail.pathname = url.pathname.replace('/img/avatar/', '/img/avatar_thumbnail/')
return thumbnail
}

/** The proxy address for a GitHub account avatar at a pixel size. */
Expand Down
10 changes: 9 additions & 1 deletion test/unit/image-proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe('avatar proxy addresses', () => {
['a GitHub avatar', 'https://avatars.githubusercontent.com/u/1?v=4'],
['a GitHub login avatar', 'https://github.com/harlan-zw.png'],
['an X avatar', 'https://pbs.twimg.com/profile_images/1/a_normal.jpg'],
['a Bluesky avatar', 'https://cdn.bsky.app/img/avatar/plain/did:plc:abc/bafk@jpeg'],
['a Bluesky avatar thumbnail', 'https://cdn.bsky.app/img/avatar_thumbnail/plain/did:plc:abc/bafk@jpeg'],
])('proxies %s', (_label, src) => {
const path = avatarProxyUrl(src)!
expect(path.startsWith('/_img/avatar?')).toBe(true)
Expand All @@ -85,6 +85,14 @@ describe('avatar proxy addresses', () => {
expect(parseImageProxyRequest('avatar', { url: src })).toEqual({ _tag: 'NotFound', reason: 'not-avatar' })
})

it('proxies a full-size Bluesky avatar as its thumbnail', () => {
const path = avatarProxyUrl('https://cdn.bsky.app/img/avatar/plain/did:plc:abc/bafk@jpeg')!
expect(parseImageProxyRequest('avatar', queryOf(path))).toEqual({
_tag: 'Ok',
request: avatar('https://cdn.bsky.app/img/avatar_thumbnail/plain/did:plc:abc/bafk@jpeg'),
})
})

it('builds a GitHub login avatar address the route accepts', () => {
const parsed = parseImageProxyRequest('avatar', queryOf(githubAvatarProxyUrl('antfu', 64)))
expect(parsed).toEqual({ _tag: 'Ok', request: avatar('https://github.com/antfu.png?size=64') })
Expand Down
Loading