mirror of
https://github.com/andwati/andwati-nextgen.git
synced 2026-10-18 05:32:52 +00:00
- Port adammaj.com layout, sidebar and 3D bookshelf from Chakra to Tailwind v4, matched to measured pixel values - Pages: About, Writing, Reading (bookshelf), Portfolio; content exported to markdown with YAML frontmatter, KaTeX math, callouts and YouTube embeds - Shiki (github-light) code highlighting via rehype-pretty-code - SEO: per-page metadata and canonicals, JSON-LD, generated OG images, sitemap, robots, RSS/JSON feeds, llms.txt - Umami analytics via PUBLIC_UMAMI_SCRIPT_URL / PUBLIC_UMAMI_WEBSITE_ID - Prettier + cspell for content, husky/lint-staged pre-commit hook - Multi-stage Dockerfile (Node 24, pnpm, standalone output) for Dokploy - Remove unused Chakra, emotion, framer-motion, posthog, next-seo and MDX deps
96 lines
2.7 KiB
TypeScript
96 lines
2.7 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { notFound } from "next/navigation";
|
|
import { breadcrumbs, JsonLd } from "@/components/JsonLd";
|
|
import { Mdx } from "@/components/Mdx";
|
|
import { byline } from "@/lib/books";
|
|
import { getBooks } from "@/lib/content";
|
|
import { abs, site } from "@/lib/site";
|
|
|
|
export function generateStaticParams() {
|
|
return getBooks().map((b) => ({ slug: b.slug }));
|
|
}
|
|
|
|
export async function generateMetadata({
|
|
params,
|
|
}: PageProps<"/reading/[slug]">): Promise<Metadata> {
|
|
const { slug } = await params;
|
|
const book = getBooks().find((b) => b.slug === slug);
|
|
if (!book) return {};
|
|
const url = `/reading/${slug}`;
|
|
const description = byline(book.data) || site.description;
|
|
return {
|
|
title: book.data.title,
|
|
description,
|
|
keywords: book.data.tags,
|
|
alternates: { canonical: url },
|
|
openGraph: {
|
|
type: "article",
|
|
url,
|
|
title: book.data.title,
|
|
description,
|
|
siteName: site.name,
|
|
locale: site.locale,
|
|
},
|
|
twitter: {
|
|
card: "summary_large_image",
|
|
title: book.data.title,
|
|
description,
|
|
},
|
|
};
|
|
}
|
|
|
|
export default async function Book({ params }: PageProps<"/reading/[slug]">) {
|
|
const { slug } = await params;
|
|
const book = getBooks().find((b) => b.slug === slug);
|
|
if (!book) notFound();
|
|
const url = abs(`/reading/${slug}`);
|
|
const item = {
|
|
"@type": book.data.kind === "paper" ? "ScholarlyArticle" : "Book",
|
|
name: book.data.title,
|
|
url: book.data.url,
|
|
author: book.data.authors?.map((name) => ({ "@type": "Person", name })),
|
|
};
|
|
const reviewLd = book.data.rating
|
|
? {
|
|
"@context": "https://schema.org",
|
|
"@type": "Review",
|
|
url,
|
|
name: `Review: ${book.data.title}`,
|
|
datePublished: book.data.dateFinished,
|
|
author: { "@type": "Person", name: site.name, url: site.baseUrl },
|
|
itemReviewed: item,
|
|
reviewRating: {
|
|
"@type": "Rating",
|
|
ratingValue: book.data.rating,
|
|
bestRating: 5,
|
|
worstRating: 1,
|
|
},
|
|
}
|
|
: null;
|
|
return (
|
|
<article className="flex flex-col gap-2">
|
|
<JsonLd
|
|
data={[
|
|
...(reviewLd ? [reviewLd] : []),
|
|
breadcrumbs([
|
|
{ name: "Home", url: site.baseUrl },
|
|
{ name: "Reading", url: abs("/reading") },
|
|
{ name: book.data.title, url },
|
|
]),
|
|
]}
|
|
/>
|
|
<div className="flex flex-col">
|
|
<h1 className="text-[36px] leading-[1.2] font-bold">
|
|
{book.data.title}
|
|
</h1>
|
|
<p className="mt-2 text-[20px] leading-[30px] text-gray-400">
|
|
{byline(book.data)}
|
|
</p>
|
|
</div>
|
|
<div className="prose">
|
|
<Mdx source={book.content} />
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|