Files
spsays-training/app/(app)/glossary/[letter]/page.tsx
T

166 lines
4.7 KiB
TypeScript

export const dynamic = "force-dynamic";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getAllGlossaryEntries } from "@/lib/api";
import { buildOgMeta, extractQuoteFromHl } from "@/lib/og";
import { GlossaryClient } from "../glossary-client";
import { JsonLd } from "@/components/seo/json-ld";
import { articleJsonLd } from "@/lib/jsonld";
// ---------------------------------------------------------------------------
// Diacritic → base letter mapping
// ---------------------------------------------------------------------------
const DIACRITIC_TO_BASE: Record<string, string> = {
Ā: "A",
Ä: "A",
Ī: "I",
É: "I",
Ū: "U",
Ü: "U",
Ṛ: "R",
Ṝ: "R",
Ś: "S",
Ç: "S",
Ṣ: "S",
Ñ: "N",
Ṇ: "N",
Ṅ: "N",
Ṭ: "T",
Ḍ: "D",
Ḥ: "H",
};
function getBaseLetter(term: string): string {
if (!term) return "?";
const first = term[0].toUpperCase();
return DIACRITIC_TO_BASE[first] || first;
}
// ---------------------------------------------------------------------------
// Metadata
// ---------------------------------------------------------------------------
export async function generateMetadata({
params,
searchParams,
}: {
params: Promise<{ letter: string }>;
searchParams: Promise<Record<string, string | string[] | undefined>>;
}): Promise<Metadata> {
const { letter } = await params;
const sp = await searchParams;
const l = letter.toUpperCase();
// If a specific term is referenced via ?term= param, show it in the OG image
const termParam = typeof sp.term === "string" ? sp.term : null;
if (termParam) {
const allEntries = await getAllGlossaryEntries();
const entry = allEntries.find(
(e) => e.term.toLowerCase() === termParam.toLowerCase(),
);
if (entry) {
// Strip HTML tags from definition for OG quote
const plainDef = entry.definitionHtml
.replace(/<[^>]*>/g, "")
.replace(/&[^;]+;/g, " ")
.trim();
// Check for ?hl= highlight param — extract actual selected text from definition
const hlQuote = extractQuoteFromHl(sp, [entry.definitionHtml]);
return buildOgMeta({
title: entry.term,
detail: "Glossary — Prabhupāda Says",
quote:
hlQuote ||
(plainDef.length > 260 ? plainDef.slice(0, 257) + "..." : plainDef),
});
}
}
// Check for ?hl= without ?term= — try to extract quote from all entries for this letter
const hlQuote = extractQuoteFromHl(sp);
if (hlQuote) {
return buildOgMeta({
title: `Glossary — ${l}`,
detail: "Sanskrit & Vedic terms",
quote: hlQuote,
});
}
// Default: letter-level OG
return buildOgMeta({
title: `Glossary — ${l}`,
detail: `Sanskrit & Vedic terms starting with ${l}`,
quote:
"A comprehensive glossary of Sanskrit and Vedic terms from the books of Śrīla Prabhupāda.",
});
}
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default async function GlossaryLetterPage({
params,
}: {
params: Promise<{ letter: string }>;
}) {
const { letter: rawLetter } = await params;
const activeLetter = rawLetter.toUpperCase();
// Validate — single letter A-Z
if (!/^[A-Z]$/.test(activeLetter)) notFound();
// Fetch all entries (cached by Next.js request dedup within a single render)
const allEntries = await getAllGlossaryEntries();
// Group all entries by base letter
const byLetter: Record<string, typeof allEntries> = {};
for (const entry of allEntries) {
const base = getBaseLetter(entry.term);
if (!byLetter[base]) byLetter[base] = [];
byLetter[base].push(entry);
}
// All available letters (sorted)
const letters = Object.keys(byLetter).sort();
// Entries for the active letter
const letterEntries = byLetter[activeLetter] || [];
if (letterEntries.length === 0) notFound();
// Prepare client data — only the active letter's entries
const clientEntries = letterEntries.map((e) => ({
term: e.term,
definition: e.definition,
definition_html: e.definitionHtml,
}));
// Letter counts for the nav
const letterCounts: Record<string, number> = {};
for (const [l, entries] of Object.entries(byLetter)) {
letterCounts[l] = entries.length;
}
return (
<div className="min-h-screen bg-background">
<JsonLd
data={articleJsonLd({
path: `/glossary/${rawLetter}`,
headline: `Glossary — ${activeLetter}`,
isPartOf: "Glossary of Sanskrit & Vedic Terms",
})}
/>
<GlossaryClient
entries={clientEntries}
activeLetter={activeLetter}
letters={letters}
letterCounts={letterCounts}
/>
</div>
);
}