/** * OG metadata for the /share/[code] page. * * Given a resolved long path (`/books/bg/18/66?hl=…`), this dispatches to the * correct page-specific OG builder so social-link previews on shortened URLs * look identical to previews on the canonical destination URL. * * The destination page's own `generateMetadata` is not invoked from `/share`, * so we replicate the equivalent fetch + `extractQuoteFromHl` + `ogFor*` * pipeline here. On any failure we fall back to a site-level default so the * link is still scrape-safe. */ import type { Metadata } from "next"; import { buildOgMeta, extractQuoteFromHl, ogDefault, ogForBook, ogForChapter, ogForEssay, ogForJournal, ogForLetter, ogForMagazine, ogForOtherDocument, ogForPost, ogForProfile, ogForTranscription, ogForVerse, } from "@/lib/og"; import { BOOK_NAMES, CC_LILAS } from "@/lib/constants"; import { bff } from "@/lib/bff"; import { getSingleVerse, getProseChapter } from "@/lib/api"; import { getLetter } from "@/lib/letters"; import { getTranscription } from "@/lib/transcriptions"; import { getJournalChapterBySlug, slugToBookCode as journalSlugToBookCode, } from "@/lib/journals"; import { getMagazineIssue, slugToBookCode as magazineSlugToBookCode, } from "@/lib/magazines"; import { getOtherDocumentChapter, slugToBookCode as otherDocSlugToBookCode, } from "@/lib/other-documents"; // --------------------------------------------------------------------------- // Path parsing // --------------------------------------------------------------------------- // Verse-bearing books with a `/{chapter}/{verse}` leaf (no division segment). const VERSE_BOOKS_NO_DIVISION = new Set([ "bg", "sb1962", "bs", "mm", "nbs", ]); // Books whose verse routes have an extra division segment (canto for SB, līlā for CC). const VERSE_BOOKS_WITH_DIVISION = new Set(["sb", "cc"]); // Books where the leaf is a single segment (mantra / text slug). const VERSE_BOOKS_SINGLE_SEGMENT = new Set(["iso", "noi"]); function splitPath(pathname: string): string[] { return pathname .split("/") .filter(Boolean) .map((s) => { try { return decodeURIComponent(s); } catch { return s; } }); } // --------------------------------------------------------------------------- // Main entrypoint // --------------------------------------------------------------------------- /** * Build OG metadata for a /share/[code] page given the resolved long path. * Never throws — any error falls back to the site default. */ export async function buildShareOgMeta(fullPath: string): Promise { try { const u = new URL("http://x" + fullPath); const parts = splitPath(u.pathname); const sp = u.searchParams; if (parts.length === 0) return ogDefault(); switch (parts[0]) { case "books": return await ogForBookPath(parts, sp); case "letters": return parts[1] ? await ogForLetterPath(parts[1], sp) : ogDefault(); case "transcriptions": return parts[1] ? await ogForTranscriptionPath(parts[1], sp) : ogDefault(); case "essays": return parts[1] ? await ogForEssayPath(parts[1], sp) : ogDefault(); case "journals": return parts.length >= 3 ? await ogForJournalPath(parts[1], parts[2], sp) : ogDefault(); case "magazines": return parts.length >= 3 ? await ogForMagazinePath(parts[1], parts[2], sp) : ogDefault(); case "other-documents": return parts.length >= 3 ? await ogForOtherDocPath(parts[1], parts[2], sp) : ogDefault(); case "u": // /u/ → profile share // /u//posts#post- → post share (post id in u.hash) return await ogForProfilePath(parts, u.hash); default: return ogWithFallbackQuote("Prabhupāda Says", sp); } } catch { return ogDefault(); } } // --------------------------------------------------------------------------- // Per-type dispatchers // --------------------------------------------------------------------------- async function ogForBookPath( parts: string[], sp: URLSearchParams, ): Promise { if (parts.length < 2) return ogDefault(); const code = parts[1]; // Verse with division (SB canto, CC līlā): /books/sb/{canto}/{ch}/{v} if (VERSE_BOOKS_WITH_DIVISION.has(code) && parts.length >= 5) { return await ogForVersePath(code, parts[2], parts[3], parts[4], sp); } // Verse without division: /books/{code}/{ch}/{v} if (VERSE_BOOKS_NO_DIVISION.has(code) && parts.length >= 4) { return await ogForVersePath(code, null, parts[2], parts[3], sp); } // ISO / NOI: /books/{code}/{slug-or-mantra} if (VERSE_BOOKS_SINGLE_SEGMENT.has(code) && parts.length >= 3) { // Treat the leaf as a "verse"-like slug. extractQuoteFromHl falls back to // prefix-suffix if we can't fetch paragraphs cheaply. return ogWithFallbackQuote(BOOK_NAMES[code] || code.toUpperCase(), sp); } // KB: /books/kb/{volume}/{slug} if (code === "kb" && parts.length >= 4) { const volume = Number.parseInt(parts[2], 10); if (Number.isFinite(volume)) { const chapter = await getProseChapter("kb", volume, parts[3]); const paragraphs = chapter?.paragraphs; const quote = extractQuoteFromHl(sp, paragraphs); if (quote) { return buildOgMeta({ title: BOOK_NAMES.kb, detail: chapter?.title, quote, }); } return ogForChapter("kb", chapter?.title ?? ""); } } // Generic prose book: /books/{code}/{slug} if (parts.length >= 3) { const chapter = await getProseChapter(code, 0, parts[2]); const paragraphs = chapter?.paragraphs; const quote = extractQuoteFromHl(sp, paragraphs); if (quote) { return buildOgMeta({ title: BOOK_NAMES[code] || code.toUpperCase(), detail: chapter?.title, quote, }); } return ogForChapter(code, chapter?.title ?? ""); } return ogForBook(code); } async function ogForVersePath( code: string, division: string | null, chapter: string, verse: string, sp: URLSearchParams, ): Promise { const chapterNum = Number.parseInt(chapter, 10); const verseNum = Number.parseInt(verse, 10); if (!Number.isFinite(chapterNum) || !Number.isFinite(verseNum)) { return ogForBook(code); } const v = await getSingleVerse(code, division, chapterNum, verseNum); const paragraphs = v ? [v.translation, ...v.purport] : undefined; const quote = extractQuoteFromHl(sp, paragraphs); if (quote) { const title = BOOK_NAMES[code] || code.toUpperCase(); let detail: string; if (code === "sb" && division) { detail = `Canto ${division}, Chapter ${chapter}, Verse ${verse}`; } else if (code === "cc" && division) { const lila = CC_LILAS[division] || division; detail = `${lila}, Chapter ${chapter}, Verse ${verse}`; } else { detail = `Chapter ${chapter}, Verse ${verse}`; } return buildOgMeta({ title, detail, quote }); } return ogForVerse(code, { division: division ?? undefined, chapter: chapterNum, verse: verseNum, }); } async function ogForLetterPath( slug: string, sp: URLSearchParams, ): Promise { const letter = await getLetter(slug); const paragraphs = letter?.paragraphs; const quote = extractQuoteFromHl(sp, paragraphs); const recipient = letter?.recipientDisplay ?? "(Unknown)"; const meta = letter?.date && letter?.location ? `${letter.date} • ${letter.location}` : letter?.date || letter?.location || undefined; if (quote) { return buildOgMeta({ title: `Letter to ${recipient}`, detail: meta, quote, }); } return ogForLetter(recipient, meta); } async function ogForTranscriptionPath( slug: string, sp: URLSearchParams, ): Promise { const t = await getTranscription(slug); const paragraphs = t?.content ?.filter((b) => b.format === "paragraph" || b.format === "quote") .map((b) => b.text); const quote = extractQuoteFromHl(sp, paragraphs); const name = t?.name ?? "Transcription"; const meta = t?.date && t?.place ? `${t.date} • ${t.place}` : t?.date || t?.place || undefined; if (quote) { return buildOgMeta({ title: name, detail: meta, quote }); } return ogForTranscription(name, meta); } type EssayDoc = { slug: string; title: string; author: string; year: number | null; group: string | null; paragraphs: string[]; }; async function ogForEssayPath( slug: string, sp: URLSearchParams, ): Promise { const doc = await bff(`/essays/${slug}`); const paragraphs = doc?.paragraphs; const quote = extractQuoteFromHl(sp, paragraphs); if (quote) { return buildOgMeta({ title: doc?.title ?? "Essay", detail: "Essay by Śrīla Prabhupāda", quote, }); } return ogForEssay(doc?.title ?? "Essay"); } async function ogForJournalPath( bookSlug: string, chapterSlug: string, sp: URLSearchParams, ): Promise { const bookCode = journalSlugToBookCode(bookSlug); const ch = await getJournalChapterBySlug(bookCode, chapterSlug); const paragraphs = ch?.paragraphs; const quote = extractQuoteFromHl(sp, paragraphs); // The fetched chapter type doesn't carry the parent journal title; use the // shared BOOK_NAMES map as the canonical display name. const journalTitle = BOOK_NAMES[bookCode] ?? bookSlug; if (quote) { return buildOgMeta({ title: journalTitle, detail: ch?.title, quote, }); } return ogForJournal(journalTitle, ch?.title); } async function ogForMagazinePath( bookSlug: string, issueSlug: string, sp: URLSearchParams, ): Promise { const bookCode = magazineSlugToBookCode(bookSlug); const issue = await getMagazineIssue(bookCode, issueSlug); const paragraphs = issue?.paragraphs; const quote = extractQuoteFromHl(sp, paragraphs); const magazineTitle = BOOK_NAMES[bookCode] ?? bookSlug; if (quote) { return buildOgMeta({ title: magazineTitle, detail: issue?.title, quote, }); } return ogForMagazine(magazineTitle, issue?.title); } async function ogForOtherDocPath( bookSlug: string, chapterPart: string, sp: URLSearchParams, ): Promise { const bookCode = otherDocSlugToBookCode(bookSlug); const chapterNum = Number.parseInt(chapterPart, 10); const docTitle = BOOK_NAMES[bookCode] ?? bookSlug; if (!Number.isFinite(chapterNum)) { return ogForOtherDocument(docTitle); } const ch = await getOtherDocumentChapter(bookCode, chapterNum); const paragraphs = ch?.paragraphs; const quote = extractQuoteFromHl(sp, paragraphs); if (quote) { return buildOgMeta({ title: docTitle, detail: ch?.title, quote, }); } return ogForOtherDocument(docTitle, ch?.title); } // --------------------------------------------------------------------------- // Fallback // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- // Profile + post share dispatch // --------------------------------------------------------------------------- type PublicProfileForOg = { handle: string; name: string; post_count?: number; }; type PostForOg = { id: string; caption: string; source_title?: string | null; author_name?: string | null; author_handle?: string | null; }; /** * Resolve `/u/` and `/u//posts#post-` share targets * to the correct OG card. The hash carries the post id when the share * is a single-post link (built by SharePostSheet); without a hash we * fall back to a profile card for the handle. */ async function ogForProfilePath( parts: string[], hash: string, ): Promise { if (parts.length < 2) return ogDefault(); const handle = parts[1]; // Post share: /u//posts with `#post-` fragment. if (parts.length >= 3 && parts[2] === "posts") { const m = hash.match(/^#post-(.+)$/); if (m) { const postID = m[1]; try { const post = await bff( `/posts/${encodeURIComponent(postID)}`, { revalidate: 60 }, ); if (post) return ogForPost(post); } catch { // fall through to profile-level card } } // No post id → treat as a feed share for the profile. } // Profile share: /u/ try { const profile = await bff( `/u/${encodeURIComponent(handle)}`, { revalidate: 60 }, ); if (profile) { return ogForProfile({ handle: profile.handle, name: profile.name, postCount: profile.post_count, }); } } catch { // fall through } return ogDefault(); } function ogWithFallbackQuote( title: string, sp: URLSearchParams, ): Metadata { // No paragraphs available — fall back to prefix … suffix reconstruction. const quote = extractQuoteFromHl(sp); if (quote) { return buildOgMeta({ title, quote }); } return ogDefault(); }