Files

453 lines
13 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,
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<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();
case "u":
// /u/<handle> → profile share
// /u/<handle>/posts#post-<id> → 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<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
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// 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/<handle>` and `/u/<handle>/posts#post-<id>` 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<Metadata> {
if (parts.length < 2) return ogDefault();
const handle = parts[1];
// Post share: /u/<handle>/posts with `#post-<id>` fragment.
if (parts.length >= 3 && parts[2] === "posts") {
const m = hash.match(/^#post-(.+)$/);
if (m) {
const postID = m[1];
try {
const post = await bff<PostForOg>(
`/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/<handle>
try {
const profile = await bff<PublicProfileForOg>(
`/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();
}