feat: share collection URL

This commit is contained in:
gosangam
2026-06-14 01:29:08 +05:30
parent b04ed1d3aa
commit 3accc753d0
5 changed files with 414 additions and 85 deletions
+215
View File
@@ -0,0 +1,215 @@
/* eslint-disable @next/next/no-img-element -- Satori (ImageResponse) only
renders raw <img>; next/image doesn't work inside OG image generation. */
/**
* OG card for a public collection (`/c/<id>`).
*
* Renders a 1200×630 image with the collection title as the centrepiece,
* an item-count line, and the cover image dimmed behind it when present
* (flat cream otherwise). Matches the post/profile card aesthetic.
*
* Data comes from the public BFF collection endpoint. The server fetch is
* unauthenticated, so private collections return 403 → we 404 here (a
* private collection never unfurls, which is the correct privacy stance).
*/
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 CollectionEnvelope = {
collection: {
id: string;
title: string;
description: string | null;
cover_url: string | null;
is_public: boolean;
item_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 };
}
export async function GET(
_request: Request,
context: { params: Promise<{ id: string }> },
) {
try {
const { id } = await context.params;
const data = await bff<CollectionEnvelope>(
`/collections/${encodeURIComponent(id)}`,
{ revalidate: 60 },
).catch(() => null);
const c = data?.collection;
if (!c || !c.is_public) {
return new Response("Collection not found", { status: 404 });
}
const fonts = await loadFonts();
const title = c.title.length > 90 ? c.title.slice(0, 87) + "…" : c.title;
const description = c.description?.trim()
? c.description.length > 160
? c.description.slice(0, 157) + "…"
: c.description
: "";
const count = `${c.item_count} post${c.item_count === 1 ? "" : "s"}`;
const bgImageUrl = c.cover_url;
const hasImage = !!bgImageUrl;
const textPrimary = hasImage ? "#ffffff" : "#2d2418";
const textSecondary = hasImage ? "rgba(255,255,255,0.85)" : "#5a4a3a";
const accent = hasImage ? "#ffd9a8" : "#c47a5a";
return new ImageResponse(
(
<div
style={{
width: 1200,
height: 630,
display: "flex",
flexDirection: "column",
backgroundColor: hasImage ? "#1a1410" : "#faf5f0",
fontFamily: "Noto Sans, sans-serif",
position: "relative",
padding: 80,
}}
>
{hasImage ? (
<>
<img
alt=""
src={bgImageUrl}
width={1200}
height={630}
style={{
position: "absolute",
top: 0,
left: 0,
width: 1200,
height: 630,
objectFit: "cover",
filter: "brightness(0.45)",
}}
/>
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: 1200,
height: 630,
background:
"linear-gradient(to top, rgba(0,0,0,0.75), rgba(0,0,0,0.1))",
}}
/>
</>
) : null}
{/* Eyebrow */}
<div
style={{
display: "flex",
fontSize: 24,
fontWeight: 700,
letterSpacing: 6,
textTransform: "uppercase",
color: accent,
zIndex: 1,
}}
>
Collection
</div>
{/* Title — the centrepiece */}
<div
style={{
display: "flex",
fontSize: title.length > 48 ? 60 : 76,
fontWeight: 700,
color: textPrimary,
lineHeight: 1.15,
marginTop: 24,
maxHeight: 320,
overflow: "hidden",
zIndex: 1,
}}
>
{title}
</div>
{description ? (
<div
style={{
display: "flex",
fontSize: 28,
color: textSecondary,
lineHeight: 1.4,
marginTop: 24,
maxHeight: 120,
overflow: "hidden",
zIndex: 1,
}}
>
{description}
</div>
) : null}
<div style={{ flex: 1, display: "flex" }} />
{/* Footer: count + branding */}
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
zIndex: 1,
}}
>
<div style={{ fontSize: 24, color: textSecondary, display: "flex" }}>
{count}
</div>
<div style={{ fontSize: 22, color: accent, fontWeight: 700 }}>
prabhupadasays.org
</div>
</div>
</div>
),
{
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("Collection OG image generation failed:", e);
return new Response("Failed to generate OG image", { status: 500 });
}
}