Skip to content

Commit 584b7a7

Browse files
committed
perf: use proxy for gravatar
1 parent 4048ab6 commit 584b7a7

File tree

5 files changed

+15
-50
lines changed

5 files changed

+15
-50
lines changed

app/components/User/Avatar.vue

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,8 @@ const props = defineProps<{
33
username: string
44
}>()
55
6-
const { data: gravatarUrl } = useLazyFetch('/api/gravatar', {
7-
query: {
8-
username: computed(() => props.username),
9-
size: 128,
10-
},
11-
transform: res => res.url,
6+
const { data: gravatarUrl } = useLazyFetch(() => `/api/gravatar/${props.username}`, {
7+
transform: res => (res.hash ? `/_avatar/${res.hash}?s=128&d=404` : null),
128
})
139
</script>
1410

nuxt.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ export default defineNuxtConfig({
105105
'/api/registry/docs/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } },
106106
'/api/registry/file/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } },
107107
'/api/registry/files/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } },
108+
'/_avatar/**': {
109+
isr: 3600,
110+
proxy: {
111+
to: 'https://www.gravatar.com/avatar/**',
112+
},
113+
},
108114
// static pages
109115
'/about': { prerender: true },
110116
'/settings': { prerender: true },
Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,23 @@ function getQueryParam(event: H3Event, key: string): string {
1313

1414
export default defineCachedEventHandler(
1515
async event => {
16-
const rawUsername = getQueryParam(event, 'username')
17-
const rawSize = getQueryParam(event, 'size')
16+
const rawUsername = getRouterParam(event, 'username')
1817

1918
try {
20-
const { username, size } = v.parse(GravatarQuerySchema, {
19+
const { username } = v.parse(GravatarQuerySchema, {
2120
username: rawUsername,
22-
size: rawSize ? rawSize : undefined,
2321
})
2422

25-
const dataUrl = await getGravatarFromUsername(username, size ?? 80)
23+
const hash = await getGravatarFromUsername(username)
2624

27-
if (!dataUrl) {
25+
if (!hash) {
2826
throw createError({
2927
statusCode: 404,
3028
message: ERROR_GRAVATAR_EMAIL_UNAVAILABLE,
3129
})
3230
}
3331

34-
return { url: dataUrl }
32+
return { hash }
3533
} catch (error: unknown) {
3634
handleApiError(error, {
3735
statusCode: 502,

server/utils/gravatar.ts

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,13 @@
1-
import { Buffer } from 'node:buffer'
21
import { createHash } from 'node:crypto'
32
import { fetchUserEmail } from '#server/utils/npm'
43

5-
const DEFAULT_GRAVATAR_SIZE = 80
6-
7-
export async function getGravatarFromUsername(
8-
username: string,
9-
size: number = DEFAULT_GRAVATAR_SIZE,
10-
): Promise<string | null> {
4+
export async function getGravatarFromUsername(username: string): Promise<string | null> {
115
const handle = username.trim()
126
if (!handle) return null
137

148
const email = await fetchUserEmail(handle)
159
if (!email) return null
1610

1711
const trimmedEmail = email.trim().toLowerCase()
18-
const md5Hash = createHash('md5').update(trimmedEmail).digest('hex')
19-
const gravatarUrl = `https://www.gravatar.com/avatar/${md5Hash}?s=${size}&d=404`
20-
21-
try {
22-
const response = await fetch(gravatarUrl)
23-
if (!response.ok) return null
24-
25-
const contentType = response.headers.get('content-type') || 'image/png'
26-
const arrayBuffer = await response.arrayBuffer()
27-
const base64 = Buffer.from(arrayBuffer).toString('base64')
28-
return `data:${contentType};base64,${base64}`
29-
} catch {
30-
return null
31-
}
12+
return createHash('md5').update(trimmedEmail).digest('hex')
3213
}

shared/schemas/user.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,6 @@ export const NpmUsernameSchema = v.pipe(
1919
*/
2020
export const GravatarQuerySchema = v.object({
2121
username: NpmUsernameSchema,
22-
size: v.optional(
23-
v.pipe(
24-
v.union([v.number(), v.string()]),
25-
v.transform(value => {
26-
if (typeof value === 'string') {
27-
const trimmed = value.trim()
28-
return /^\d+$/.test(trimmed) ? Number(trimmed) : Number.NaN
29-
}
30-
return value
31-
}),
32-
v.check(value => !Number.isNaN(value), 'Invalid size'),
33-
v.number(),
34-
v.minValue(16),
35-
v.maxValue(512),
36-
),
37-
),
3822
})
3923

4024
/** @public */

0 commit comments

Comments
 (0)