diff --git a/src/app/story/[storylineId]/og/route.tsx b/src/app/story/[storylineId]/og/route.tsx index 3477dc10..2c2280c0 100644 --- a/src/app/story/[storylineId]/og/route.tsx +++ b/src/app/story/[storylineId]/og/route.tsx @@ -4,10 +4,30 @@ import { createServerClient, type Storyline } from "../../../../../lib/supabase" import { getTokenPrice } from "../../../../../lib/price"; import { lookupByAddress } from "../../../../../lib/farcaster"; import { RESERVE_LABEL, STORY_FACTORY } from "../../../../../lib/contracts/constants"; +import { formatPrice } from "../../../../../lib/format"; import { truncateAddress } from "../../../../../lib/utils"; export const runtime = "edge"; +async function loadFont(): Promise { + try { + // Fetch with no User-Agent → Google returns TTF (truetype) format + // ImageResponse supports ttf/otf/woff but NOT woff2 + const res = await fetch( + "https://fonts.googleapis.com/css2?family=Lora:wght@700&display=swap", + ); + const css = await res.text(); + const match = + css.match(/src:\s*url\(([^)]+)\)\s*format\(['"]truetype['"]\)/) ?? + css.match(/src:\s*url\(([^)]+)\)\s*format\(['"]woff['"]\)/); + if (!match?.[1]) return null; + const fontRes = await fetch(match[1]); + return fontRes.arrayBuffer(); + } catch { + return null; + } +} + export async function GET( _request: Request, { params }: { params: Promise<{ storylineId: string }> }, @@ -38,15 +58,26 @@ export async function GET( const sl = storyline as Storyline; - const [priceInfo, farcasterProfile] = await Promise.all([ + const [priceInfo, farcasterProfile, fontData] = await Promise.all([ sl.token_address ? getTokenPrice(sl.token_address as Address) : null, lookupByAddress(sl.writer_address).catch(() => null), + loadFont(), ]); const reserveLabel = RESERVE_LABEL; const priceDisplay = priceInfo - ? `${priceInfo.pricePerToken} ${reserveLabel}` + ? `${formatPrice(priceInfo.pricePerToken)} ${reserveLabel}` : null; + const authorName = farcasterProfile + ? `@${farcasterProfile.username}` + : truncateAddress(sl.writer_address); + const plotLabel = sl.plot_count === 1 ? "plot" : "plots"; + const titleDisplay = + sl.title.length > 70 ? `${sl.title.slice(0, 67)}...` : sl.title; + + const fonts = fontData + ? [{ name: "Lora", data: fontData, weight: 700 as const }] + : []; return new ImageResponse( ( @@ -55,67 +86,194 @@ export async function GET( width: "100%", height: "100%", display: "flex", - flexDirection: "column", - justifyContent: "space-between", - padding: "60px", - backgroundColor: "#E8DFD0", - color: "#2C1810", - fontFamily: "Georgia, serif", + backgroundColor: "#DDD3C2", + padding: "32px", + fontFamily: fontData ? "Lora" : "Georgia, serif", }} > - {/* Top: branding */} -
- PlotLink -
- - {/* Center: title */} + {/* Notebook page */}
+ {/* Red margin line */} +
+ + {/* Ruled lines */}
- {sl.title.length > 80 ? `${sl.title.slice(0, 77)}...` : sl.title} + {Array.from({ length: 16 }).map((_, i) => ( +
+ ))}
-
- {/* Bottom: metadata */} -
- by {farcasterProfile ? `@${farcasterProfile.username}` : truncateAddress(sl.writer_address)} - - {sl.plot_count} {sl.plot_count === 1 ? "plot" : "plots"} - - {priceDisplay && Price: {priceDisplay}} + {/* Content overlay */} +
+ {/* Top: PlotLink branding */} +
+
+ PlotLink +
+ {sl.genre && ( +
+ {sl.genre} +
+ )} +
+ + {/* Center: Story title */} +
+
40 ? "42px" : "52px", + fontWeight: 700, + color: "#2C1810", + lineHeight: 1.2, + display: "flex", + }} + > + {titleDisplay} +
+
+ by {authorName} +
+
+ + {/* Bottom: metadata bar */} +
+
+ + {sl.plot_count} {plotLabel} + + {priceDisplay && ( + + {priceDisplay} + + )} +
+
+ plotlink.xyz +
+
+
), { width: 1200, height: 630, + fonts, }, ); }