/** * 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 = { 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 = { 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 | 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/`). */ 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//posts#post-` 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: 1200, alt: reference }], siteName: "Prabhupāda Says", }, twitter: { card: "summary_large_image" as const, title, description, images: [imageUrl], }, }; } /** OG metadata for a public collection (`/c/`). */ 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: 1200, alt: title }], siteName: "Prabhupāda Says", }, twitter: { card: "summary_large_image" as const, title, description, images: [imageUrl], }, }; }