fix: og image generation

This commit is contained in:
gosangam
2026-05-06 01:19:06 +05:30
parent 0a82a12580
commit b81b18632d
13 changed files with 464 additions and 179 deletions
+10
View File
@@ -1,5 +1,7 @@
import type { Metadata } from "next";
import Link from "next/link";
import { PageHeader } from "@/components/page-header";
import { buildOgMeta } from "@/lib/og";
import {
BookOpen,
BrainCircuit,
@@ -45,6 +47,14 @@ const PHASES = [
},
];
export function generateMetadata(): Metadata {
return buildOgMeta({
title: "About — Prabhupāda Says",
quote:
"Built by a small team of Prabhupādānugas — preserving and presenting the teachings of Śrīla Prabhupāda exactly as he gave them, without alteration.",
});
}
export default function AboutPage() {
return (
<div className="min-h-screen bg-background">
+170 -167
View File
@@ -4,215 +4,218 @@ import { join } from "node:path";
export const runtime = "nodejs";
// Cache the font data at module level so it's only loaded once
let geistBoldData: ArrayBuffer | null = null;
let geistRegularData: ArrayBuffer | null = null;
async function loadFonts() {
if (!geistBoldData) {
// Fetch Geist Bold from Google Fonts CDN
const boldRes = await fetch(
"https://cdn.jsdelivr.net/fontsource/fonts/geist-sans@latest/latin-700-normal.woff",
);
geistBoldData = await boldRes.arrayBuffer();
}
if (!geistRegularData) {
const regularRes = await fetch(
"https://cdn.jsdelivr.net/fontsource/fonts/geist-sans@latest/latin-400-normal.woff",
);
geistRegularData = await regularRes.arrayBuffer();
}
return { bold: geistBoldData, regular: geistRegularData };
}
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
try {
const { searchParams } = new URL(request.url);
const title = searchParams.get("title") || "Prabhupāda Says";
const detail = searchParams.get("detail") || "";
const quote = searchParams.get("quote") || "";
const title = searchParams.get("title") || "Prabhupāda Says";
const detail = searchParams.get("detail") || "";
const rawQuote = searchParams.get("quote") || "";
// Truncate for readability and to prevent overflow
const displayTitle = title.length > 60 ? title.slice(0, 57) + "..." : title;
const displayDetail =
detail.length > 80 ? detail.slice(0, 77) + "..." : detail;
const displayQuote = quote.length > 220 ? quote.slice(0, 217) + "..." : quote;
// Fallback: if no quote provided, generate one from context
const quote =
rawQuote ||
(title === "Prabhupāda Says"
? "Browse the original, unedited books, letters, and teachings of His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda — presented as they are."
: `Read ${title}${detail ? ` — ${detail}` : ""} by His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda`);
// Load the Prabhupāda image as base64
const imgData = await readFile(
join(process.cwd(), "public/images/og-left-prabhupada-reading.png"),
"base64",
);
const imgSrc = `data:image/png;base64,${imgData}`;
// Truncate for readability and to prevent overflow
const displayTitle = title.length > 60 ? title.slice(0, 57) + "..." : title;
const displayDetail =
detail.length > 80 ? detail.slice(0, 77) + "..." : detail;
const displayQuote =
quote.length > 220 ? quote.slice(0, 217) + "..." : quote;
return new ImageResponse(
<div
style={{
width: "1200",
height: "630",
display: "flex",
flexDirection: "row",
backgroundColor: "#faf5f0",
fontFamily: "sans-serif",
overflow: "hidden",
position: "relative",
}}
>
{/* Left side — image + branding */}
// Load fonts and image
const fonts = await loadFonts();
const imgData = await readFile(
join(process.cwd(), "public/images/og-left-prabhupada-reading.png"),
"base64",
);
const imgSrc = `data:image/png;base64,${imgData}`;
return new ImageResponse(
<div
style={{
width: "460",
height: "630",
width: 1200,
height: 630,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "flex-end",
flexDirection: "row",
backgroundColor: "#faf5f0",
fontFamily: "Geist, sans-serif",
overflow: "hidden",
position: "relative",
flexShrink: 0,
}}
>
{/* Decorative quote mark */}
{/* Left side — image + branding */}
<div
style={{
position: "absolute",
top: "30",
left: "30",
fontSize: "180",
fontWeight: "900",
color: "#e8b4b8",
lineHeight: "1",
width: 460,
height: 630,
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
padding: 20,
}}
>
&#x201C;
{/* Prabhupāda image — already contains quote mark and branding */}
<img
src={imgSrc}
width={420}
height={590}
style={{
objectFit: "contain",
objectPosition: "center",
}}
/>
</div>
{/* Prabhupāda image */}
<img
src={imgSrc}
width="420"
height="500"
style={{
objectFit: "contain",
objectPosition: "bottom",
marginBottom: "0",
}}
/>
{/* Branding at the bottom */}
{/* Right side — content */}
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
alignItems: "center",
paddingBottom: "24",
marginTop: "-10",
justifyContent: "center",
paddingTop: 50,
paddingRight: 60,
paddingBottom: 50,
paddingLeft: 60,
gap: 16,
}}
>
<span
style={{
fontSize: "36",
color: "#d4929a",
fontStyle: "italic",
fontWeight: "600",
}}
>
Prabhupāda
</span>
<span
style={{
fontSize: "20",
color: "#d4929a",
fontWeight: "700",
letterSpacing: "2",
marginTop: "-2",
}}
>
says
</span>
</div>
</div>
{/* Right side — content */}
<div
style={{
flex: "1",
display: "flex",
flexDirection: "column",
justifyContent: "center",
padding: "50 60 50 30",
gap: "16",
}}
>
{/* Title */}
<div
style={{
fontSize: "44",
fontWeight: "800",
color: "#2d2418",
lineHeight: "1.15",
display: "flex",
overflow: "hidden",
maxHeight: "102",
}}
>
{displayTitle}
</div>
{/* Detail (chapter, verse, etc.) */}
{displayDetail && (
{/* Title */}
<div
style={{
fontSize: "22",
fontWeight: "600",
color: "#5a4a3a",
lineHeight: "1.3",
fontSize: 36,
fontWeight: 700,
color: "#2d2418",
lineHeight: 1.15,
display: "flex",
overflow: "hidden",
maxHeight: "30",
maxHeight: 84,
}}
>
{displayDetail}
{displayTitle}
</div>
)}
{/* Quote text */}
{displayQuote && (
<div
style={{
display: "flex",
flexDirection: "column",
marginTop: "16",
overflow: "hidden",
maxHeight: "180",
}}
>
{/* Detail (chapter, verse, etc.) */}
{displayDetail ? (
<div
style={{
fontSize: "24",
color: "#3d3228",
lineHeight: "1.5",
fontSize: 22,
fontWeight: 600,
color: "#5a4a3a",
lineHeight: 1.3,
display: "flex",
overflow: "hidden",
maxHeight: 30,
}}
>
{displayQuote}
{displayDetail}
</div>
</div>
)}
) : null}
{/* Read more */}
<div
style={{
display: "flex",
alignItems: "center",
gap: "6",
marginTop: "16",
}}
>
<span
{/* Quote text */}
{displayQuote ? (
<div
style={{
display: "flex",
flexDirection: "column",
marginTop: 16,
overflow: "hidden",
maxHeight: 180,
}}
>
<div
style={{
fontSize: 24,
color: "#3d3228",
lineHeight: 1.5,
display: "flex",
overflow: "hidden",
}}
>
{displayQuote}
</div>
</div>
) : null}
{/* Read more */}
<div
style={{
fontSize: "18",
fontWeight: "700",
color: "#5a4a3a",
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 6,
marginTop: 16,
}}
>
Read more at
</span>
<span
style={{
fontSize: "18",
fontWeight: "700",
color: "#c47a5a",
}}
>
prabhupadasays.org
</span>
<span
style={{
fontSize: 18,
fontWeight: 700,
color: "#5a4a3a",
}}
>
Read more at
</span>
<span
style={{
fontSize: 18,
fontWeight: 700,
color: "#c47a5a",
}}
>
prabhupadasays.org
</span>
</div>
</div>
</div>
</div>,
{
width: 1200,
height: 630,
},
);
</div>,
{
width: 1200,
height: 630,
fonts: [
{
name: "Geist",
data: fonts.regular,
style: "normal" as const,
weight: 400,
},
{
name: "Geist",
data: fonts.bold,
style: "normal" as const,
weight: 700,
},
],
},
);
} catch (e) {
console.error("OG image generation failed:", e);
return new Response("Failed to generate OG image", { status: 500 });
}
}
+6
View File
@@ -1,7 +1,9 @@
export const dynamic = "force-dynamic";
import type { Metadata } from "next";
import Link from "next/link";
import { PageHeader } from "@/components/page-header";
import { ogForDisciples } from "@/lib/og";
import {
getTotalDiscipleCount,
getFilteredDisciples,
@@ -14,6 +16,10 @@ import {
import type { SuffixFilter } from "@/lib/disciples";
import { DisciplesBrowser } from "./disciples-browser";
export async function generateMetadata(): Promise<Metadata> {
return ogForDisciples();
}
const VALID_SUFFIXES: SuffixFilter[] = ["all", "Dasa", "Devi Dasi"];
function parseSuffix(raw: string | undefined): SuffixFilter {
+11
View File
@@ -1,10 +1,21 @@
export const dynamic = "force-dynamic";
import type { Metadata } from "next";
import Link from "next/link";
import { PageHeader } from "@/components/page-header";
import { buildOgMeta } from "@/lib/og";
import { prisma } from "@/lib/db";
import { EssaysGrid } from "./essays-grid";
export function generateMetadata(): Metadata {
return buildOgMeta({
title: "Essays",
detail: "Standalone essays by Śrīla Prabhupāda",
quote:
"Foundational essays on the science of Kṛṣṇa consciousness by His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda.",
});
}
async function getAllEssays() {
return prisma.essay.findMany({
orderBy: [{ group: "asc" }, { order: "asc" }],
+11
View File
@@ -1,7 +1,18 @@
import type { Metadata } from "next";
import Link from "next/link";
import { PageHeader } from "@/components/page-header";
import { GlossaryClient } from "./glossary-client";
import { getAllGlossaryEntries } from "@/lib/api";
import { buildOgMeta } from "@/lib/og";
export function generateMetadata(): Metadata {
return buildOgMeta({
title: "Glossary",
detail: "2,700+ Sanskrit & Vedic terms",
quote:
"A comprehensive glossary of Sanskrit and Vedic terms from the books of Śrīla Prabhupāda.",
});
}
export default async function GlossaryPage() {
const entries = await getAllGlossaryEntries();
+40 -7
View File
@@ -1,18 +1,38 @@
export const dynamic = "force-dynamic";
import type { Metadata } from "next";
import Link from "next/link";
import { PageHeader } from "@/components/page-header";
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card";
import { buildOgMeta } from "@/lib/og";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { getAllJournals, bookCodeToSlug } from "@/lib/journals";
export function generateMetadata(): Metadata {
return buildOgMeta({
title: "Journals",
detail: "Personal diaries and journals of Śrīla Prabhupāda",
quote:
"From the personal journals of Śrīla Prabhupāda — a window into the daily life and mission of the Founder-Ācārya of ISKCON.",
});
}
export default async function JournalsPage() {
const journals = await getAllJournals();
return (
<div className="min-h-screen bg-background">
<PageHeader>
<Link href="/" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
<Link
href="/"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
&larr; Home
</Link>
<h1 className="mt-3 text-3xl font-bold text-foreground">Journals</h1>
@@ -24,20 +44,33 @@ export default async function JournalsPage() {
<main className="max-w-5xl mx-auto px-6 py-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{journals.map((journal) => (
<Link key={journal.book} href={`/journals/${bookCodeToSlug(journal.book)}`} className="block h-full">
<Link
key={journal.book}
href={`/journals/${bookCodeToSlug(journal.book)}`}
className="block h-full"
>
<Card className="h-full transition-all hover:shadow-md hover:ring-foreground/20">
<CardHeader className="pb-2">
<CardTitle className="text-base font-bold leading-snug">{journal.title}</CardTitle>
<CardTitle className="text-base font-bold leading-snug">
{journal.title}
</CardTitle>
{journal.description && (
<CardDescription className="text-xs line-clamp-2">{journal.description}</CardDescription>
<CardDescription className="text-xs line-clamp-2">
{journal.description}
</CardDescription>
)}
</CardHeader>
<CardContent className="pt-0">
<div className="flex flex-wrap items-center gap-2 text-sm">
{journal.year && <Badge variant="secondary" className="text-xs">{journal.year}</Badge>}
{journal.year && (
<Badge variant="secondary" className="text-xs">
{journal.year}
</Badge>
)}
{journal.totalChapters > 0 && (
<span className="text-muted-foreground text-xs">
{journal.totalChapters} {journal.totalChapters === 1 ? "chapter" : "chapters"}
{journal.totalChapters}{" "}
{journal.totalChapters === 1 ? "chapter" : "chapters"}
</span>
)}
</div>
+14
View File
@@ -1,9 +1,23 @@
export const dynamic = "force-dynamic";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getLettersByLocation } from "@/lib/letters";
import { ogForLettersByLocation } from "@/lib/og";
import { FilterYearBrowser } from "../../filter-year-browser";
export async function generateMetadata({
params,
}: {
params: Promise<{ name: string }>;
}): Promise<Metadata> {
const { name: encodedName } = await params;
const name = decodeURIComponent(encodedName);
const letters = await getLettersByLocation(name);
if (letters.length === 0) return {};
return ogForLettersByLocation(name, letters.length);
}
export default async function LettersByLocationPage({
params,
}: {
+11
View File
@@ -1,7 +1,9 @@
export const dynamic = "force-dynamic";
import type { Metadata } from "next";
import Link from "next/link";
import { PageHeader } from "@/components/page-header";
import { buildOgMeta } from "@/lib/og";
import {
getLetterYears,
getLetterRecipients,
@@ -11,6 +13,15 @@ import {
import { LettersNav } from "./letters-nav";
import { LettersBrowser } from "./letters-browser";
export function generateMetadata(): Metadata {
return buildOgMeta({
title: "Letters",
detail: "6,800+ letters (1947–1977)",
quote:
"Personal correspondence by Śrīla Prabhupāda — guiding disciples, Godbrothers, and well-wishers in the science of Kṛṣṇa consciousness.",
});
}
export default async function LettersPage({
searchParams,
}: {
+14
View File
@@ -1,9 +1,23 @@
export const dynamic = "force-dynamic";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getLettersByRecipient } from "@/lib/letters";
import { ogForLettersByRecipient } from "@/lib/og";
import { FilterYearBrowser } from "../../filter-year-browser";
export async function generateMetadata({
params,
}: {
params: Promise<{ name: string }>;
}): Promise<Metadata> {
const { name: encodedName } = await params;
const name = decodeURIComponent(encodedName);
const letters = await getLettersByRecipient(name);
if (letters.length === 0) return {};
return ogForLettersByRecipient(name, letters.length);
}
export default async function LettersByRecipientPage({
params,
}: {
+14
View File
@@ -1,9 +1,23 @@
export const dynamic = "force-dynamic";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getLettersByYear } from "@/lib/letters";
import { ogForLettersByYear } from "@/lib/og";
import { YearMonthBrowser } from "./year-month-browser";
export async function generateMetadata({
params,
}: {
params: Promise<{ year: string }>;
}): Promise<Metadata> {
const { year: yearStr } = await params;
const year = parseInt(yearStr, 10);
if (isNaN(year)) return {};
const letters = await getLettersByYear(year);
return ogForLettersByYear(year, letters.length);
}
export default async function LettersByYearPage({
params,
}: {
+11
View File
@@ -1,5 +1,6 @@
export const dynamic = "force-dynamic";
import type { Metadata } from "next";
import Link from "next/link";
import { PageHeader } from "@/components/page-header";
import {
@@ -11,6 +12,16 @@ import {
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { getAllMagazines, bookCodeToSlug } from "@/lib/magazines";
import { buildOgMeta } from "@/lib/og";
export function generateMetadata(): Metadata {
return buildOgMeta({
title: "Magazines",
detail: "Back to Godhead & other periodicals",
quote:
"Back to Godhead — the magazine founded by Śrīla Prabhupāda in 1944 to present the timeless science of Kṛṣṇa consciousness.",
});
}
export default async function MagazinesPage() {
const magazines = await getAllMagazines();
+11
View File
@@ -1,9 +1,20 @@
export const dynamic = "force-dynamic";
import type { Metadata } from "next";
import Link from "next/link";
import { PageHeader } from "@/components/page-header";
import { getAllOtherDocuments, bookCodeToSlug } from "@/lib/other-documents";
import { OtherDocumentsGrid } from "./other-documents-grid";
import { buildOgMeta } from "@/lib/og";
export function generateMetadata(): Metadata {
return buildOgMeta({
title: "Other Documents",
detail: "Miscellaneous analyses & writings",
quote:
"Writings and analyses by His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda.",
});
}
export default async function OtherDocumentsPage() {
const documents = await getAllOtherDocuments();