| 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 | } |