···11import React from 'react'
2233+// @NOTE satori does not currently support webp, see vercel/satori#273
34function detectMime(buf: Buffer): string {
45 if (buf[0] === 0xff && buf[1] === 0xd8) return 'image/jpeg'
56 if (buf[0] === 0x89 && buf[1] === 0x50) return 'image/png'
+10-4
bskyogcard/src/routes/starter-pack.tsx
···11import assert from 'node:assert'
2233import React from 'react'
44-import {AppBskyGraphDefs, AtUri} from '@atproto/api'
44+import {type AppBskyGraphDefs, AtUri} from '@atproto/api'
55import resvg from '@resvg/resvg-js'
66-import {Express} from 'express'
66+import {type Express} from 'express'
77import satori from 'satori'
8899import {
···1111 STARTERPACK_HEIGHT,
1212 STARTERPACK_WIDTH,
1313} from '../components/StarterPack.js'
1414-import {AppContext} from '../context.js'
1414+import {type AppContext} from '../context.js'
1515import {httpLogger} from '../logger.js'
1616import {loadEmojiAsSvg} from '../util.js'
1717import {handler, originVerifyMiddleware} from './util.js'
···8383}
84848585async function getImage(url: string) {
8686- const response = await fetch(url)
8686+ const response = await fetch(ensureJpeg(url))
8787 const arrayBuf = await response.arrayBuffer() // must drain body even if it will be discarded
8888 if (response.status !== 200) return null
8989 return Buffer.from(arrayBuf)
9090+}
9191+9292+// CDN URLs end with @jpeg, @webp, or no extension (which may default to webp).
9393+// We want to ensure the image URLs we use are for jpegs, required for compat with satori.
9494+function ensureJpeg(url: string) {
9595+ return url.replace(/(@[a-z]{3,5})?$/, '@jpeg')
9096}
91979298const hideAvatarLabels = new Set([