From 10da513c6de0abc1b99844cb228f2a6b814a7025 Mon Sep 17 00:00:00 2001 From: Devin Ivy Date: Fri, 6 Mar 2026 23:30:25 -0500 Subject: [PATCH] ogcard: ensure we use jpeg images for satori compat --- bskyogcard/src/components/Img.tsx | 1 + bskyogcard/src/routes/starter-pack.tsx | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/bskyogcard/src/components/Img.tsx b/bskyogcard/src/components/Img.tsx index 733e7e2262a..cd63bc6a91d 100644 --- a/bskyogcard/src/components/Img.tsx +++ b/bskyogcard/src/components/Img.tsx @@ -1,5 +1,6 @@ import React from 'react' +// @NOTE satori does not currently support webp, see vercel/satori#273 function detectMime(buf: Buffer): string { if (buf[0] === 0xff && buf[1] === 0xd8) return 'image/jpeg' if (buf[0] === 0x89 && buf[1] === 0x50) return 'image/png' diff --git a/bskyogcard/src/routes/starter-pack.tsx b/bskyogcard/src/routes/starter-pack.tsx index f6b6dc20f4b..99a3e73fe5d 100644 --- a/bskyogcard/src/routes/starter-pack.tsx +++ b/bskyogcard/src/routes/starter-pack.tsx @@ -1,9 +1,9 @@ import assert from 'node:assert' import React from 'react' -import {AppBskyGraphDefs, AtUri} from '@atproto/api' +import {type AppBskyGraphDefs, AtUri} from '@atproto/api' import resvg from '@resvg/resvg-js' -import {Express} from 'express' +import {type Express} from 'express' import satori from 'satori' import { @@ -11,7 +11,7 @@ import { STARTERPACK_HEIGHT, STARTERPACK_WIDTH, } from '../components/StarterPack.js' -import {AppContext} from '../context.js' +import {type AppContext} from '../context.js' import {httpLogger} from '../logger.js' import {loadEmojiAsSvg} from '../util.js' import {handler, originVerifyMiddleware} from './util.js' @@ -83,12 +83,18 @@ export default function (ctx: AppContext, app: Express) { } async function getImage(url: string) { - const response = await fetch(url) + const response = await fetch(ensureJpeg(url)) const arrayBuf = await response.arrayBuffer() // must drain body even if it will be discarded if (response.status !== 200) return null return Buffer.from(arrayBuf) } +// CDN URLs end with @jpeg, @webp, or no extension (which may default to webp). +// We want to ensure the image URLs we use are for jpegs, required for compat with satori. +function ensureJpeg(url: string) { + return url.replace(/(@[a-z]{3,5})?$/, '@jpeg') +} + const hideAvatarLabels = new Set([ '!hide', '!warn',