development
tsx 165 lines 4.37 KB
Raw
1 import { notFound } from "next/navigation";
2 import { allPosts } from "contentlayer/generated";
3 import type { Metadata } from "next";
4 import Link from "next/link";
5 import { Mdx } from "@/components/mdx-components";
6 import { StructuredData } from "@/components/structured-data";
7 import siteMetadata from "@/content/siteMetadata";
8 import { absoluteUrl, genPageMetadata } from "@/app/seo";
9
10 interface PostProps {
11 params: Promise<{
12 slug: string[];
13 }>;
14 }
15
16 async function getPostFromParams(params: Awaited<PostProps["params"]>) {
17 const slug = params?.slug?.join("/");
18 const post = allPosts
19 .filter((post) => post.published)
20 .find((post) => post.slugAsParams === slug);
21
22 return post ?? null;
23 }
24
25 export async function generateMetadata(props: PostProps): Promise<Metadata> {
26 const params = await props.params;
27 const post = await getPostFromParams(params);
28
29 if (!post) {
30 return {};
31 }
32
33 return genPageMetadata({
34 title: post.title,
35 description: post.description,
36 path: post.slug,
37 type: "article",
38 openGraph: {
39 type: "article",
40 publishedTime: post.date,
41 modifiedTime: post.lastmod,
42 authors: [siteMetadata.author],
43 },
44 });
45 }
46
47 export async function generateStaticParams() {
48 return allPosts
49 .filter((post) => post.published)
50 .map((post) => ({
51 slug: post.slugAsParams.split("/"),
52 }));
53 }
54
55 export default async function PostPage(props: PostProps) {
56 const params = await props.params;
57 const post = await getPostFromParams(params);
58
59 if (!post) {
60 notFound();
61 }
62
63 const postStructuredData = {
64 "@context": "https://schema.org",
65 "@graph": [
66 {
67 "@type": "BlogPosting",
68 headline: post.title,
69 description: post.description,
70 url: absoluteUrl(post.slug),
71 datePublished: post.date,
72 dateModified: post.lastmod,
73 author: {
74 "@type": "Organization",
75 name: siteMetadata.author,
76 },
77 publisher: {
78 "@id": absoluteUrl("/#organization"),
79 },
80 mainEntityOfPage: {
81 "@type": "WebPage",
82 "@id": absoluteUrl(post.slug),
83 },
84 },
85 {
86 "@type": "BreadcrumbList",
87 itemListElement: [
88 {
89 "@type": "ListItem",
90 position: 1,
91 name: "Home",
92 item: absoluteUrl("/"),
93 },
94 {
95 "@type": "ListItem",
96 position: 2,
97 name: "Blog",
98 item: absoluteUrl("/posts"),
99 },
100 {
101 "@type": "ListItem",
102 position: 3,
103 name: post.title,
104 item: absoluteUrl(post.slug),
105 },
106 ],
107 },
108 ],
109 };
110
111 return (
112 <div className="mx-auto max-w-3xl px-6 py-12 lg:px-8">
113 <StructuredData data={postStructuredData} />
114
115 <nav className="mb-8 flex items-center gap-2 text-xs text-slate-400 dark:text-slate-600">
116 <Link
117 href="/"
118 className="transition-colors hover:text-slate-700 dark:hover:text-slate-300"
119 >
120 Home
121 </Link>
122 <span>/</span>
123 <Link
124 href="/posts"
125 className="transition-colors hover:text-slate-700 dark:hover:text-slate-300"
126 >
127 Blog
128 </Link>
129 <span>/</span>
130 <span className="text-slate-600 dark:text-slate-400">{post.title}</span>
131 </nav>
132
133 <article className="prose dark:prose-invert max-w-none">
134 <h1 className="mb-2">{post.title}</h1>
135 <div className="mb-4 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-slate-500 dark:text-slate-400">
136 <time dateTime={post.date}>
137 Published{" "}
138 {new Date(post.date).toLocaleDateString("en-US", {
139 year: "numeric",
140 month: "long",
141 day: "numeric",
142 })}
143 </time>
144 {post.lastmod && (
145 <time dateTime={post.lastmod}>
146 Updated{" "}
147 {new Date(post.lastmod).toLocaleDateString("en-US", {
148 year: "numeric",
149 month: "long",
150 day: "numeric",
151 })}
152 </time>
153 )}
154 </div>
155 {post.description && (
156 <p className="text-xl mt-0 text-slate-700 dark:text-slate-200">
157 {post.description}
158 </p>
159 )}
160 <hr className="my-4" />
161 <Mdx code={post.body.code} />
162 </article>
163 </div>
164 );
165 }