/** * OG card for a user profile (`/u/`). * * Renders a 1200×630 image with avatar (or initials fallback), display * name, @handle, and a stats line (post count + join year). Designed * to match the chapter-card OG aesthetic so a feed of links from * different parts of the site looks coherent in WhatsApp / Twitter * previews. * * Data comes from the public BFF profile endpoint — no auth required. * Failures (missing profile, BFF unreachable) return 404 / 500 instead * of a generic image so social scrapers know to fall back to a * higher-level card; the share/[code]'s `ogDefault()` shape handles * that case. */ import { ImageResponse } from "next/og"; import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { bff } from "@/lib/bff"; export const runtime = "nodejs"; type PublicProfile = { handle: string; name: string; avatar_url: string | null; joined_at: string; post_count: number; }; let notoRegularData: Buffer | null = null; let notoBoldData: Buffer | null = null; async function loadFonts() { const fontsDir = join(process.cwd(), "public/fonts/Noto_Sans/static"); if (!notoRegularData) { notoRegularData = await readFile(join(fontsDir, "NotoSans-Regular.ttf")); } if (!notoBoldData) { notoBoldData = await readFile(join(fontsDir, "NotoSans-Bold.ttf")); } return { regular: notoRegularData, bold: notoBoldData }; } function initialsFor(name: string): string { return ( name .split(/\s+/) .filter(Boolean) .map((p) => p[0]) .slice(0, 2) .join("") .toUpperCase() || "?" ); } function formatJoined(iso: string): string { const d = new Date(iso); if (Number.isNaN(d.getTime())) return ""; return `${d.toLocaleString("en-US", { month: "short" })} ${d.getFullYear()}`; } export async function GET( _request: Request, context: { params: Promise<{ handle: string }> }, ) { try { const { handle } = await context.params; const profile = await bff( `/u/${encodeURIComponent(handle)}`, { revalidate: 60 }, ).catch(() => null); if (!profile) { return new Response("Profile not found", { status: 404 }); } const fonts = await loadFonts(); const displayName = profile.name.length > 40 ? profile.name.slice(0, 37) + "…" : profile.name; const stats = [ `${profile.post_count} post${profile.post_count === 1 ? "" : "s"}`, profile.joined_at ? `Joined ${formatJoined(profile.joined_at)}` : null, ] .filter(Boolean) .join(" · "); return new ImageResponse( (
{/* Top row: avatar + identity block */}
{profile.avatar_url ? ( ) : (
{initialsFor(profile.name)}
)}
{displayName}
@{profile.handle}
{stats ? (
{stats}
) : null}
{/* Footer: branding */}
Read quotes shared on prabhupadasays.org
), { width: 1200, height: 630, fonts: [ { name: "Noto Sans", data: fonts.regular, style: "normal" as const, weight: 400, }, { name: "Noto Sans", data: fonts.bold, style: "normal" as const, weight: 700, }, ], }, ); } catch (e) { console.error("Profile OG image generation failed:", e); return new Response("Failed to generate OG image", { status: 500 }); } }