Skip to content

Commit b2e724b

Browse files
committed
og image fix
1 parent a75adc5 commit b2e724b

File tree

1 file changed

+84
-58
lines changed

1 file changed

+84
-58
lines changed

web/src/pages/og-image.ts

Lines changed: 84 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,25 @@ interface RecordData {
4141
color: string;
4242
}
4343

44-
async function fetchAvatarDataUri(url: string): Promise<string> {
44+
async function resolveAvatarUrl(did: string): Promise<string> {
45+
if (!did) return "";
46+
try {
47+
const res = await fetch(
48+
`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(did)}`,
49+
);
50+
if (!res.ok) return "";
51+
const profile = await res.json();
52+
const avatar = profile.avatar || "";
53+
if (!avatar) return "";
54+
return avatar.replace(/@[a-z]+$/, "@jpeg") +
55+
(/@[a-z]+$/.test(avatar) ? "" : "@jpeg");
56+
} catch {
57+
return "";
58+
}
59+
}
60+
61+
async function fetchAvatarDataUri(did: string): Promise<string> {
62+
const url = await resolveAvatarUrl(did);
4563
if (!url) return "";
4664
try {
4765
const res = await fetch(url, { headers: { "User-Agent": "margin.at/og" } });
@@ -67,7 +85,7 @@ async function fetchRecordData(uri: string): Promise<RecordData | null> {
6785
const did = author.did || "";
6886
const authorName = handle ? `@${handle}` : did || "someone";
6987
const displayName = author.displayName || handle || did || "someone";
70-
const avatarURL = await fetchAvatarDataUri(author.avatar || "");
88+
const avatarURL = await fetchAvatarDataUri(author.did || "");
7189
const targetSource = item.target?.source || item.url || item.source || "";
7290
const domain = targetSource
7391
? (() => {
@@ -149,7 +167,7 @@ async function fetchRecordData(uri: string): Promise<RecordData | null> {
149167
const did = author.did || "";
150168
const authorName = handle ? `@${handle}` : did || "someone";
151169
const displayName = author.displayName || handle || did || "someone";
152-
const avatarURL = await fetchAvatarDataUri(author.avatar || "");
170+
const avatarURL = await fetchAvatarDataUri(author.did || "");
153171

154172
return {
155173
type: "collection",
@@ -810,64 +828,72 @@ export const GET: APIRoute = async ({ url }) => {
810828
return new Response("uri parameter required", { status: 400 });
811829
}
812830

813-
const data = await fetchRecordData(uri);
814-
if (!data) {
815-
return new Response("Record not found", { status: 404 });
816-
}
831+
try {
832+
const data = await fetchRecordData(uri);
833+
if (!data) {
834+
return new Response("Record not found", { status: 404 });
835+
}
817836

818-
const fonts = loadFonts();
819-
820-
let element: unknown;
821-
switch (data.type) {
822-
case "collection":
823-
element = buildCollectionImage(data);
824-
break;
825-
case "bookmark":
826-
element = buildBookmarkImage(data);
827-
break;
828-
case "highlight":
829-
element = buildHighlightImage(data);
830-
break;
831-
case "annotation":
832-
default:
833-
element = buildAnnotationImage(data);
834-
break;
835-
}
837+
const fonts = loadFonts();
838+
839+
let element: unknown;
840+
switch (data.type) {
841+
case "collection":
842+
element = buildCollectionImage(data);
843+
break;
844+
case "bookmark":
845+
element = buildBookmarkImage(data);
846+
break;
847+
case "highlight":
848+
element = buildHighlightImage(data);
849+
break;
850+
case "annotation":
851+
default:
852+
element = buildAnnotationImage(data);
853+
break;
854+
}
836855

837-
const svg = await satori(element as React.ReactNode, {
838-
width: 1200,
839-
height: 630,
840-
fonts: [
841-
{ name: "Inter", data: fonts.regular, weight: 400, style: "normal" },
842-
{ name: "Inter", data: fonts.bold, weight: 700, style: "normal" },
843-
],
844-
loadAdditionalAsset: async (code: string, segment: string) => {
845-
if (code === "emoji") {
846-
const codepoints = [...segment]
847-
.map((c) => c.codePointAt(0)!.toString(16))
848-
.join("-");
849-
const emojiUrl = `https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/${codepoints}.svg`;
850-
try {
851-
const res = await fetch(emojiUrl);
852-
if (res.ok)
853-
return `data:image/svg+xml,${encodeURIComponent(await res.text())}`;
854-
} catch {
855-
// ignore
856+
const svg = await satori(element as React.ReactNode, {
857+
width: 1200,
858+
height: 630,
859+
fonts: [
860+
{ name: "Inter", data: fonts.regular, weight: 400, style: "normal" },
861+
{ name: "Inter", data: fonts.bold, weight: 700, style: "normal" },
862+
],
863+
loadAdditionalAsset: async (code: string, segment: string) => {
864+
if (code === "emoji") {
865+
const codepoints = [...segment]
866+
.map((c) => c.codePointAt(0)!.toString(16))
867+
.join("-");
868+
const emojiUrl = `https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/${codepoints}.svg`;
869+
try {
870+
const res = await fetch(emojiUrl);
871+
if (res.ok)
872+
return `data:image/svg+xml,${encodeURIComponent(await res.text())}`;
873+
} catch {
874+
// ignore
875+
}
856876
}
857-
}
858-
return "";
859-
},
860-
});
877+
return "";
878+
},
879+
});
861880

862-
const resvg = new Resvg(svg, {
863-
fitTo: { mode: "width", value: 1200 },
864-
});
865-
const png = resvg.render().asPng();
881+
const resvg = new Resvg(svg, {
882+
fitTo: { mode: "width", value: 1200 },
883+
});
884+
const png = resvg.render().asPng();
866885

867-
return new Response(new Uint8Array(png), {
868-
headers: {
869-
"Content-Type": "image/png",
870-
"Cache-Control": "public, max-age=86400",
871-
},
872-
});
886+
return new Response(new Uint8Array(png), {
887+
headers: {
888+
"Content-Type": "image/png",
889+
"Cache-Control": "public, max-age=86400",
890+
},
891+
});
892+
} catch (e) {
893+
console.error("[og-image] Error generating image:", e);
894+
console.error("[og-image] cwd:", process.cwd());
895+
console.error("[og-image] publicDir:", getPublicDir());
896+
const BASE_URL = process.env.BASE_URL || "https://margin.at";
897+
return Response.redirect(`${BASE_URL}/og.png`, 302);
898+
}
873899
};

0 commit comments

Comments
 (0)