509 lines
19 KiB
TypeScript
509 lines
19 KiB
TypeScript
/**
|
|
* OG Image metadata utilities.
|
|
*
|
|
* Builds the /api/og URL with appropriate query params for dynamic
|
|
* Open Graph image generation. Used by page-level metadata exports.
|
|
*/
|
|
|
|
import { BOOK_NAMES, CC_LILAS } from "@/lib/constants";
|
|
import { decodeTextFragment, extractMatchedText } from "@/lib/text-highlight";
|
|
|
|
const SITE_URL =
|
|
process.env.NEXT_PUBLIC_SITE_URL || "https://prabhupadasays.org";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export type OgParams = {
|
|
title: string;
|
|
detail?: string;
|
|
quote?: string;
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Fallback descriptions — used when no quote/description is provided
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const SITE_DESCRIPTION =
|
|
"Browse the original, unedited books, letters, and teachings of His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda — presented as they are.";
|
|
|
|
const BOOK_DESCRIPTIONS: Record<string, string> = {
|
|
bg: "The definitive guide to the science of self-realization, spoken by Lord Śrī Kṛṣṇa to His devotee Arjuna on the Battlefield of Kurukṣetra.",
|
|
sb: "The ripened fruit of the tree of Vedic literature — 18,000 verses describing the pastimes of the Supreme Lord and His devotees.",
|
|
cc: "The life and teachings of Śrī Caitanya Mahāprabhu, the most munificent incarnation of Godhead who distributed love of God freely to all.",
|
|
sb1962:
|
|
"The original 1962 edition of Śrīmad-Bhāgavatam — Śrīla Prabhupāda's first major literary contribution in English.",
|
|
iso: "Eighteen mantras revealing the essential teachings of the Vedas — the relationship between God, the living entity, and the material world.",
|
|
noi: "Eleven verses by Śrīla Rūpa Gosvāmī providing essential instructions for serious practitioners of devotional service.",
|
|
nod: "A comprehensive guide to the science of bhakti-yoga, summarizing Śrīla Rūpa Gosvāmī's Bhakti-rasāmṛta-sindhu.",
|
|
bs: "The prayers of Lord Brahmā glorifying the Supreme Lord Govinda — with translation and commentary by Śrīla Prabhupāda.",
|
|
mm: "Devotional prayers of King Kulaśekhara expressing the purest sentiments of surrender to Lord Kṛṣṇa.",
|
|
nbs: "The eternal principles of devotional service as taught by the great sage Nārada Muni.",
|
|
kb: "The enchanting pastimes of Lord Śrī Kṛṣṇa in Vṛndāvana and Dvārakā — the Supreme Personality of Godhead in His original form.",
|
|
tlc: "The sublime teachings of Lord Śrī Caitanya Mahāprabhu to Śrīla Sanātana Gosvāmī, Śrīla Rūpa Gosvāmī, and others.",
|
|
ej: "A scientific presentation of the anti-material world and how the soul can travel there.",
|
|
ek: "How to elevate consciousness from the material to the spiritual platform through the practice of Kṛṣṇa consciousness.",
|
|
mg: "An introduction to the unmatched gift of Kṛṣṇa consciousness — the path to eternal happiness and self-realization.",
|
|
bbd: "A concise exploration of the soul's journey through repeated birth and death, and the path to liberation.",
|
|
owk: "Practical guidance on the journey toward Kṛṣṇa — developing love of God through devotional service.",
|
|
rv: "The king of knowledge — understanding the supreme science of devotional service as taught in the Bhagavad-gītā.",
|
|
poy: "The perfection of the yoga system — how bhakti-yoga surpasses all other forms of yoga and meditation.",
|
|
pqpa: "Conversations between Śrīla Prabhupāda and Bob Cohen — answering life's most fundamental questions.",
|
|
ttp: "The teachings of Lord Kapila to His mother Devahūti on the science of God realization and devotional service.",
|
|
ochkm:
|
|
"The transcendental vibration of the Hare Kṛṣṇa mahā-mantra — its meaning, potency, and practice.",
|
|
hkh: "A practical handbook for understanding and practicing Kṛṣṇa consciousness in daily life.",
|
|
lcl: "Morning walks with Śrīla Prabhupāda — challenging the fundamental assumptions of modern science.",
|
|
lob: "Meditations on the beauty of nature as a reflection of the Supreme Lord's transcendental opulence.",
|
|
tlk: "Lord Kapila's instructions on Sāṅkhya philosophy and the path of devotional service.",
|
|
tqk: "The prayers and teachings of Queen Kuntī — exemplifying pure devotion in the face of adversity.",
|
|
gg: "Bengali poetic renderings of the Bhagavad-gītā by Śrīla Prabhupāda.",
|
|
mog: "Śrīla Prabhupāda's message of Godhead — presenting the Absolute Truth for the modern world.",
|
|
cbhc: "Commentary on the Śrī Caitanya-bhāgavata — the pastimes of Lord Caitanya by Vṛndāvana dāsa Ṭhākura.",
|
|
bk: "Bhagavāner Kathā — Bengali narration of the pastimes of the Supreme Lord.",
|
|
pk: "Prabhupāda-Kṛpā — the mercy of Śrīla Prabhupāda preserved in his writings.",
|
|
ssr: "Conversations and essays illuminating the timeless science of self-realization for the modern reader.",
|
|
ds: "Śrīla Prabhupāda's critique of Western philosophy from Socrates to Sartre — in light of Vedic wisdom.",
|
|
sva: "Sacred songs of the great Vaiṣṇava ācāryas — prayers and bhajanas with translations and purports.",
|
|
va: "Śrīla Prabhupāda's commentary on the Vedānta-sūtra — the conclusion of all Vedic knowledge.",
|
|
};
|
|
|
|
const CONTENT_TYPE_DESCRIPTIONS: Record<string, string> = {
|
|
letters:
|
|
"Personal correspondence by Śrīla Prabhupāda — guiding disciples, Godbrothers, and well-wishers in the science of Kṛṣṇa consciousness.",
|
|
essays:
|
|
"Foundational essays on the science of Kṛṣṇa consciousness by His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda.",
|
|
glossary:
|
|
"A comprehensive glossary of Sanskrit and Vedic terms from the books of Śrīla Prabhupāda.",
|
|
disciples:
|
|
"A directory of disciples initiated by Śrīla Prabhupāda between 1966 and 1977.",
|
|
journals:
|
|
"From the personal journals of Śrīla Prabhupāda — a window into the daily life and mission of the Founder-Ācārya of ISKCON.",
|
|
magazines:
|
|
"Back to Godhead — the magazine founded by Śrīla Prabhupāda in 1944 to present the timeless science of Kṛṣṇa consciousness.",
|
|
"other-documents":
|
|
"Writings and analyses by His Divine Grace A.C. Bhaktivedanta Swami Prabhupāda.",
|
|
transcriptions:
|
|
"Transcriptions of Śrīla Prabhupāda's lectures, morning walks, room conversations, and dialogues — the living voice of the Founder-Ācārya preserved in text.",
|
|
};
|
|
|
|
function getFallbackQuote(title: string, code?: string): string {
|
|
// Book-specific description
|
|
if (code && BOOK_DESCRIPTIONS[code]) {
|
|
return BOOK_DESCRIPTIONS[code];
|
|
}
|
|
|
|
// Content-type descriptions
|
|
if (title.startsWith("Letter to")) {
|
|
return CONTENT_TYPE_DESCRIPTIONS.letters;
|
|
}
|
|
for (const [key, desc] of Object.entries(CONTENT_TYPE_DESCRIPTIONS)) {
|
|
if (title.toLowerCase().includes(key)) {
|
|
return desc;
|
|
}
|
|
}
|
|
|
|
// Generic fallback
|
|
return SITE_DESCRIPTION;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Build the /api/og image URL
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Build a fully-qualified URL to the /api/og image endpoint.
|
|
*
|
|
* The URL is kept as short as possible — only non-empty params are included.
|
|
* The quote is truncated to 260 chars to keep URL length reasonable.
|
|
*/
|
|
export function buildOgImageUrl(params: OgParams): string {
|
|
const url = new URL("/api/og", SITE_URL);
|
|
|
|
url.searchParams.set("title", params.title);
|
|
|
|
if (params.detail) {
|
|
url.searchParams.set("detail", params.detail);
|
|
}
|
|
|
|
if (params.quote) {
|
|
const truncated =
|
|
params.quote.length > 260
|
|
? params.quote.slice(0, 257) + "..."
|
|
: params.quote;
|
|
url.searchParams.set("quote", truncated);
|
|
}
|
|
|
|
return url.toString();
|
|
}
|
|
|
|
/**
|
|
* Build OG metadata object for use in Next.js page `metadata` exports.
|
|
*
|
|
* Returns the `openGraph` and `twitter` fields to spread into `Metadata`.
|
|
*/
|
|
export function buildOgMeta(params: OgParams & { description?: string }) {
|
|
const imageUrl = buildOgImageUrl(params);
|
|
const description =
|
|
params.description ||
|
|
params.quote ||
|
|
`${params.title}${params.detail ? ` — ${params.detail}` : ""}`;
|
|
|
|
return {
|
|
openGraph: {
|
|
title: params.title,
|
|
description,
|
|
images: [
|
|
{
|
|
url: imageUrl,
|
|
width: 1200,
|
|
height: 630,
|
|
alt: params.title,
|
|
},
|
|
],
|
|
siteName: "Prabhupāda Says",
|
|
},
|
|
twitter: {
|
|
card: "summary_large_image" as const,
|
|
title: params.title,
|
|
description,
|
|
images: [imageUrl],
|
|
},
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers for common page types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** OG metadata for a book page. */
|
|
export function ogForBook(code: string) {
|
|
const title = BOOK_NAMES[code] || code.toUpperCase();
|
|
return buildOgMeta({
|
|
title,
|
|
quote: getFallbackQuote(title, code),
|
|
});
|
|
}
|
|
|
|
/** OG metadata for a verse page. */
|
|
export function ogForVerse(
|
|
code: string,
|
|
parts: { division?: string; chapter: number; verse: number },
|
|
) {
|
|
const title = BOOK_NAMES[code] || code.toUpperCase();
|
|
let detail: string;
|
|
|
|
if (code === "sb" && parts.division) {
|
|
detail = `Canto ${parts.division}, Chapter ${parts.chapter}, Verse ${parts.verse}`;
|
|
} else if (code === "cc" && parts.division) {
|
|
const lila = CC_LILAS[parts.division] || parts.division;
|
|
detail = `${lila}, Chapter ${parts.chapter}, Verse ${parts.verse}`;
|
|
} else {
|
|
detail = `Chapter ${parts.chapter}, Verse ${parts.verse}`;
|
|
}
|
|
|
|
return buildOgMeta({
|
|
title,
|
|
detail,
|
|
quote: getFallbackQuote(title, code),
|
|
});
|
|
}
|
|
|
|
/** OG metadata for a prose chapter. */
|
|
export function ogForChapter(code: string, chapterTitle: string) {
|
|
const bookTitle = BOOK_NAMES[code] || code.toUpperCase();
|
|
return buildOgMeta({
|
|
title: bookTitle,
|
|
detail: chapterTitle,
|
|
quote: getFallbackQuote(bookTitle, code),
|
|
});
|
|
}
|
|
|
|
/** OG metadata for an essay. */
|
|
export function ogForEssay(title: string) {
|
|
return buildOgMeta({
|
|
title,
|
|
detail: "Essay by Śrīla Prabhupāda",
|
|
quote: CONTENT_TYPE_DESCRIPTIONS.essays,
|
|
});
|
|
}
|
|
|
|
/** OG metadata for a letter. */
|
|
export function ogForLetter(recipient: string, meta?: string) {
|
|
return buildOgMeta({
|
|
title: `Letter to ${recipient}`,
|
|
detail: meta || undefined,
|
|
quote: CONTENT_TYPE_DESCRIPTIONS.letters,
|
|
});
|
|
}
|
|
|
|
/** OG metadata for a journal entry. */
|
|
export function ogForJournal(journalTitle: string, entryTitle?: string) {
|
|
return buildOgMeta({
|
|
title: journalTitle,
|
|
detail: entryTitle || undefined,
|
|
quote: CONTENT_TYPE_DESCRIPTIONS.journals,
|
|
});
|
|
}
|
|
|
|
/** OG metadata for a magazine issue. */
|
|
export function ogForMagazine(magazineTitle: string, issueTitle?: string) {
|
|
return buildOgMeta({
|
|
title: magazineTitle,
|
|
detail: issueTitle || undefined,
|
|
quote: CONTENT_TYPE_DESCRIPTIONS.magazines,
|
|
});
|
|
}
|
|
|
|
/** OG metadata for an other document chapter. */
|
|
export function ogForOtherDocument(docTitle: string, chapterTitle?: string) {
|
|
return buildOgMeta({
|
|
title: docTitle,
|
|
detail: chapterTitle || undefined,
|
|
quote: CONTENT_TYPE_DESCRIPTIONS["other-documents"],
|
|
});
|
|
}
|
|
|
|
/** OG metadata for a transcription. */
|
|
export function ogForTranscription(name: string, meta?: string) {
|
|
return buildOgMeta({
|
|
title: name,
|
|
detail: meta || undefined,
|
|
quote: CONTENT_TYPE_DESCRIPTIONS.transcriptions,
|
|
});
|
|
}
|
|
|
|
/** OG metadata for the transcriptions index. */
|
|
export function ogForTranscriptions() {
|
|
return buildOgMeta({
|
|
title: "Transcriptions",
|
|
detail: "Lectures & conversations",
|
|
quote: CONTENT_TYPE_DESCRIPTIONS.transcriptions,
|
|
});
|
|
}
|
|
|
|
/** OG metadata for the disciples directory. */
|
|
export function ogForDisciples() {
|
|
return buildOgMeta({
|
|
title: "Disciples of Śrīla Prabhupāda",
|
|
quote: CONTENT_TYPE_DESCRIPTIONS.disciples,
|
|
});
|
|
}
|
|
|
|
/** OG metadata for letters filtered by year. */
|
|
export function ogForLettersByYear(year: number, count: number) {
|
|
return buildOgMeta({
|
|
title: `Letters from ${year}`,
|
|
detail: `${count} ${count === 1 ? "letter" : "letters"}`,
|
|
quote: `Śrīla Prabhupāda's correspondence from ${year} — personal letters guiding disciples and well-wishers in the science of Kṛṣṇa consciousness.`,
|
|
});
|
|
}
|
|
|
|
/** OG metadata for letters filtered by recipient. */
|
|
export function ogForLettersByRecipient(name: string, count: number) {
|
|
return buildOgMeta({
|
|
title: `Letters to ${name}`,
|
|
detail: `${count} ${count === 1 ? "letter" : "letters"}`,
|
|
quote: `Śrīla Prabhupāda's personal correspondence to ${name} — instructions, blessings, and guidance in devotional service.`,
|
|
});
|
|
}
|
|
|
|
/** OG metadata for letters filtered by location. */
|
|
export function ogForLettersByLocation(location: string, count: number) {
|
|
return buildOgMeta({
|
|
title: `Letters from ${location}`,
|
|
detail: `${count} ${count === 1 ? "letter" : "letters"}`,
|
|
quote: `Śrīla Prabhupāda's letters written from ${location} — chronicling the worldwide mission of the Hare Kṛṣṇa movement.`,
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// hl= query param → OG quote extraction
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Extract a display quote from the `?hl=` search param.
|
|
*
|
|
* When `paragraphs` are provided (from the database), the full matched text
|
|
* is extracted by searching the content — giving the OG image the actual
|
|
* selected quote, not just the prefix/suffix.
|
|
*
|
|
* When `paragraphs` are not provided, falls back to reconstructing a
|
|
* readable snippet from prefix + suffix: "first few words ... last few words"
|
|
*
|
|
* Returns null if hl is missing or invalid.
|
|
*/
|
|
export function extractQuoteFromHl(
|
|
searchParams: Record<string, string | string[] | undefined> | URLSearchParams,
|
|
paragraphs?: string[],
|
|
): string | null {
|
|
const raw =
|
|
searchParams instanceof URLSearchParams
|
|
? searchParams.get("hl")
|
|
: typeof searchParams.hl === "string"
|
|
? searchParams.hl
|
|
: null;
|
|
|
|
if (!raw) return null;
|
|
|
|
const fragment = decodeTextFragment(raw);
|
|
if (!fragment) return null;
|
|
|
|
// If paragraphs are available, extract the full matched text
|
|
if (paragraphs && paragraphs.length > 0) {
|
|
const fullMatch = extractMatchedText(fragment, paragraphs);
|
|
if (fullMatch) {
|
|
// Truncate at word boundary if too long for OG image
|
|
if (fullMatch.length > 280) {
|
|
const truncated = fullMatch.slice(0, 277);
|
|
const lastSpace = truncated.lastIndexOf(" ");
|
|
return (
|
|
(lastSpace > 200 ? truncated.slice(0, lastSpace) : truncated) + "..."
|
|
);
|
|
}
|
|
return fullMatch;
|
|
}
|
|
}
|
|
|
|
// Fallback: reconstruct from prefix + suffix
|
|
const { prefix, suffix } = fragment;
|
|
if (!prefix) return null;
|
|
|
|
if (suffix && suffix !== prefix) {
|
|
return `${prefix} ... ${suffix}`;
|
|
}
|
|
|
|
return prefix;
|
|
}
|
|
|
|
/** Default OG metadata for the site. */
|
|
export function ogDefault() {
|
|
return buildOgMeta({
|
|
title: "Prabhupāda Says",
|
|
description: SITE_DESCRIPTION,
|
|
quote: SITE_DESCRIPTION,
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Social: profile + post OG cards
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// Profiles and posts get their own image-rendering routes (see
|
|
// app/api/og/profile/[handle] and app/api/og/post/[id]) so the card
|
|
// can carry avatars, author bylines, and (for posts) the attached
|
|
// image. The helpers below just produce the Metadata object pointing
|
|
// at those routes. They live here rather than inline in share-og.ts
|
|
// so non-share entrypoints (e.g. a profile/post page that adds its
|
|
// own `generateMetadata`) can reuse the same builder.
|
|
|
|
/** OG metadata for a public user profile (`/u/<handle>`). */
|
|
export function ogForProfile(p: {
|
|
handle: string;
|
|
name: string;
|
|
postCount?: number;
|
|
}) {
|
|
const imageUrl = new URL(
|
|
`/api/og/profile/${encodeURIComponent(p.handle)}`,
|
|
SITE_URL,
|
|
).toString();
|
|
const title = `${p.name} (@${p.handle})`;
|
|
const stats =
|
|
typeof p.postCount === "number"
|
|
? `${p.postCount} post${p.postCount === 1 ? "" : "s"} on Prabhupāda Says`
|
|
: "Reading shared on Prabhupāda Says";
|
|
return {
|
|
title,
|
|
description: stats,
|
|
openGraph: {
|
|
title,
|
|
description: stats,
|
|
images: [{ url: imageUrl, width: 1200, height: 630, alt: title }],
|
|
siteName: "Prabhupāda Says",
|
|
},
|
|
twitter: {
|
|
card: "summary_large_image" as const,
|
|
title,
|
|
description: stats,
|
|
images: [imageUrl],
|
|
},
|
|
};
|
|
}
|
|
|
|
/** OG metadata for a single post (`/u/<handle>/posts#post-<id>` shares). */
|
|
export function ogForPost(p: {
|
|
id: string;
|
|
caption: string;
|
|
source_title?: string | null;
|
|
author_name?: string | null;
|
|
author_handle?: string | null;
|
|
}) {
|
|
const imageUrl = new URL(
|
|
`/api/og/post/${encodeURIComponent(p.id)}`,
|
|
SITE_URL,
|
|
).toString();
|
|
const byline = p.author_handle
|
|
? `${p.author_name ?? p.author_handle} (@${p.author_handle})`
|
|
: p.author_name ?? "Prabhupāda Says";
|
|
const reference = p.source_title?.trim() || "Prabhupāda Says";
|
|
// Description is shown in some unfurlers (Slack, Discord) when the
|
|
// image fails to load — keep the caption visible there too.
|
|
const truncatedCaption =
|
|
p.caption.length > 200 ? p.caption.slice(0, 197) + "…" : p.caption;
|
|
const description = `“${truncatedCaption}” — ${reference}`;
|
|
const title = `${reference} · shared by ${byline}`;
|
|
return {
|
|
title,
|
|
description,
|
|
openGraph: {
|
|
title,
|
|
description,
|
|
images: [{ url: imageUrl, width: 1200, height: 630, alt: reference }],
|
|
siteName: "Prabhupāda Says",
|
|
},
|
|
twitter: {
|
|
card: "summary_large_image" as const,
|
|
title,
|
|
description,
|
|
images: [imageUrl],
|
|
},
|
|
};
|
|
}
|
|
|
|
/** OG metadata for a public collection (`/c/<id>`). */
|
|
export function ogForCollection(c: {
|
|
id: string;
|
|
title: string;
|
|
description?: string | null;
|
|
itemCount?: number;
|
|
}) {
|
|
const imageUrl = new URL(
|
|
`/api/og/collection/${encodeURIComponent(c.id)}`,
|
|
SITE_URL,
|
|
).toString();
|
|
const title = c.title;
|
|
const count =
|
|
typeof c.itemCount === "number"
|
|
? `${c.itemCount} post${c.itemCount === 1 ? "" : "s"}`
|
|
: "A collection";
|
|
const description =
|
|
c.description?.trim() || `${count} on Prabhupāda Says`;
|
|
return {
|
|
title,
|
|
description,
|
|
openGraph: {
|
|
title,
|
|
description,
|
|
images: [{ url: imageUrl, width: 1200, height: 630, alt: title }],
|
|
siteName: "Prabhupāda Says",
|
|
},
|
|
twitter: {
|
|
card: "summary_large_image" as const,
|
|
title,
|
|
description,
|
|
images: [imageUrl],
|
|
},
|
|
};
|
|
}
|