development
tsx 135 lines 4.19 KB
Raw
1 import { notFound } from "next/navigation";
2 import type { Metadata } from "next";
3 import { allPages } from "contentlayer/generated";
4 import { Mdx } from "@/components/mdx-components";
5 import AboutMedia from "@/components/about-media";
6 import Link from "next/link";
7 import { StructuredData } from "@/components/structured-data";
8 import { absoluteUrl, genPageMetadata } from "@/app/seo";
9
10 interface PageProps {
11 params: Promise<{
12 slug: string[];
13 }>;
14 }
15
16 async function getPageFromParams(params: Awaited<PageProps["params"]>) {
17 const slug = params?.slug?.join("/");
18 return allPages.find((page) => page.slugAsParams === slug) ?? null;
19 }
20
21 export async function generateMetadata(props: PageProps): Promise<Metadata> {
22 const params = await props.params;
23 const page = await getPageFromParams(params);
24
25 if (!page) {
26 return {};
27 }
28
29 return genPageMetadata({
30 title: page.title,
31 description: page.description,
32 path: `/${page.slugAsParams}`,
33 });
34 }
35
36 export async function generateStaticParams() {
37 return allPages.map((page) => ({
38 slug: page.slugAsParams.split("/"),
39 }));
40 }
41
42 export default async function PagePage(props: PageProps) {
43 const params = await props.params;
44 const page = await getPageFromParams(params);
45
46 if (!page) {
47 notFound();
48 }
49
50 const pagePath = `/${page.slugAsParams}`;
51
52 const pageStructuredData = {
53 "@context": "https://schema.org",
54 "@graph": [
55 {
56 "@type": "WebPage",
57 "@id": absoluteUrl(pagePath),
58 url: absoluteUrl(pagePath),
59 name: page.title,
60 description: page.description,
61 isPartOf: {
62 "@id": absoluteUrl("/#website"),
63 },
64 breadcrumb: {
65 "@id": `${absoluteUrl(pagePath)}#breadcrumb`,
66 },
67 },
68 {
69 "@type": "BreadcrumbList",
70 "@id": `${absoluteUrl(pagePath)}#breadcrumb`,
71 itemListElement: [
72 {
73 "@type": "ListItem",
74 position: 1,
75 name: "Home",
76 item: absoluteUrl("/"),
77 },
78 {
79 "@type": "ListItem",
80 position: 2,
81 name: page.title,
82 item: absoluteUrl(pagePath),
83 },
84 ],
85 },
86 ],
87 };
88
89 return (
90 <div className="mx-auto max-w-3xl px-6 py-16 lg:px-8">
91 <StructuredData data={pageStructuredData} />
92
93 <nav className="mb-10 flex items-center gap-2 text-xs text-slate-400 dark:text-slate-600">
94 <Link
95 href="/"
96 className="transition-colors hover:text-slate-700 dark:hover:text-slate-300"
97 >
98 Home
99 </Link>
100 <span>/</span>
101 <span className="text-slate-600 dark:text-slate-400">{page.title}</span>
102 </nav>
103
104 <div className="mb-12">
105 <h1 className="text-4xl font-bold tracking-tight text-slate-900 dark:text-white sm:text-5xl">
106 {page.title}
107 </h1>
108 {page.description && (
109 <p className="mt-4 text-lg leading-relaxed text-slate-500 dark:text-slate-400">
110 {page.description}
111 </p>
112 )}
113 </div>
114
115 {page.slugAsParams === "about" && (
116 <div className="mb-12">
117 <AboutMedia />
118 </div>
119 )}
120
121 <article className="prose dark:prose-invert prose-slate max-w-none prose-headings:font-semibold prose-headings:tracking-tight prose-a:text-indigo-600 dark:prose-a:text-indigo-400 prose-a:no-underline hover:prose-a:underline prose-code:rounded prose-code:bg-slate-100 dark:prose-code:bg-white/[0.06] prose-code:px-1 prose-code:py-0.5 prose-code:text-slate-800 dark:prose-code:text-slate-200 prose-code:before:content-none prose-code:after:content-none prose-pre:rounded-xl prose-pre:border prose-pre:border-slate-200 dark:prose-pre:border-white/[0.06] prose-pre:bg-[#0D1117] prose-hr:border-slate-200 dark:prose-hr:border-white/[0.06] prose-strong:text-slate-900 dark:prose-strong:text-white">
122 <Mdx code={page.body.code} />
123 </article>
124
125 <p className="mt-16 border-t border-slate-200 dark:border-white/[0.06] pt-6 text-xs text-slate-400 dark:text-slate-600">
126 Last updated{" "}
127 {new Date(page.lastmod).toLocaleDateString("en-US", {
128 year: "numeric",
129 month: "long",
130 day: "numeric",
131 })}
132 </p>
133 </div>
134 );
135 }