379 lines
11 KiB
TypeScript
379 lines
11 KiB
TypeScript
/**
|
|
* 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,
|
|
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<Metadata> {
|
|
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();
|
|
default:
|
|
return ogWithFallbackQuote("Prabhupāda Says", sp);
|
|
}
|
|
} catch {
|
|
return ogDefault();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Per-type dispatchers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function ogForBookPath(
|
|
parts: string[],
|
|
sp: URLSearchParams,
|
|
): Promise<Metadata> {
|
|
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<Metadata> {
|
|
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<Metadata> {
|
|
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<Metadata> {
|
|
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<Metadata> {
|
|
const doc = await bff<EssayDoc>(`/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<Metadata> {
|
|
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<Metadata> {
|
|
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<Metadata> {
|
|
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
|
|
// ---------------------------------------------------------------------------
|
|
|
|
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();
|
|
}
|