|
1 | | -import fs from "fs/promises"; |
| 1 | +import type { Metadata } from "next"; |
2 | 2 | import { notFound } from "next/navigation"; |
3 | | -import { Metadata } from "next"; |
4 | | -import { getProcessedMarkdownFile } from "~/utils/getProcessedMarkdownFile"; |
5 | | -import { getFileMetadata } from "~/utils/getFileMetadata"; |
6 | | -import { BLOG_DIRECTORY } from "~/(home)/blog/blogDirectory"; |
| 3 | +import { importPage } from "nextra/pages"; |
| 4 | +import type { BlogData } from "../blogSchema"; |
| 5 | +import { getAllBlogs, getBlogBySlug } from "../readBlogs"; |
7 | 6 |
|
8 | 7 | type Params = { |
9 | 8 | params: Promise<{ |
10 | 9 | slug: string; |
11 | 10 | }>; |
12 | 11 | }; |
13 | 12 |
|
14 | | -const BlogPost = async ({ params }: Params) => { |
| 13 | +type ImportedPage = Awaited<ReturnType<typeof importPage>>; |
| 14 | + |
| 15 | +const hasPrimaryHeading = (sourceCode: string): boolean => |
| 16 | + /(^|\n)#\s+\S/m.test(sourceCode); |
| 17 | + |
| 18 | +const loadBlogPage = async (slug: string): Promise<ImportedPage> => |
| 19 | + importPage(["blog", slug]); |
| 20 | + |
| 21 | +const getMetadataDescription = ({ |
| 22 | + description, |
| 23 | + metadata, |
| 24 | +}: { |
| 25 | + description?: string; |
| 26 | + metadata: Metadata; |
| 27 | +}): string | undefined => |
| 28 | + description ?? |
| 29 | + (typeof metadata.description === "string" ? metadata.description : undefined); |
| 30 | + |
| 31 | +const buildBlogPostMetadata = ({ |
| 32 | + blog, |
| 33 | + pageMetadata, |
| 34 | +}: { |
| 35 | + blog: BlogData; |
| 36 | + pageMetadata: Metadata; |
| 37 | +}): Metadata => { |
| 38 | + const description = getMetadataDescription({ |
| 39 | + description: blog.description, |
| 40 | + metadata: pageMetadata, |
| 41 | + }); |
| 42 | + |
| 43 | + return { |
| 44 | + ...pageMetadata, |
| 45 | + title: blog.title, |
| 46 | + description, |
| 47 | + authors: [{ name: blog.author }], |
| 48 | + keywords: blog.tags.length ? blog.tags : pageMetadata.keywords, |
| 49 | + alternates: { |
| 50 | + ...pageMetadata.alternates, |
| 51 | + canonical: `/blog/${blog.slug}`, |
| 52 | + }, |
| 53 | + openGraph: { |
| 54 | + ...pageMetadata.openGraph, |
| 55 | + type: "article", |
| 56 | + title: blog.title, |
| 57 | + description, |
| 58 | + publishedTime: blog.date, |
| 59 | + authors: [blog.author], |
| 60 | + tags: blog.tags.length ? blog.tags : undefined, |
| 61 | + url: `/blog/${blog.slug}`, |
| 62 | + }, |
| 63 | + twitter: { |
| 64 | + ...pageMetadata.twitter, |
| 65 | + title: blog.title, |
| 66 | + description, |
| 67 | + }, |
| 68 | + }; |
| 69 | +}; |
| 70 | + |
| 71 | +const BlogPost = async ({ params }: Params): Promise<React.ReactElement> => { |
| 72 | + const { slug } = await params; |
| 73 | + const blog = await getBlogBySlug(slug); |
| 74 | + |
| 75 | + if (!blog) { |
| 76 | + notFound(); |
| 77 | + } |
| 78 | + |
15 | 79 | try { |
16 | | - const { slug } = await params; |
17 | | - const { data, contentHtml } = await getProcessedMarkdownFile({ |
18 | | - slug, |
19 | | - directory: BLOG_DIRECTORY, |
20 | | - }); |
| 80 | + const { default: MDXContent, sourceCode } = await loadBlogPage(slug); |
| 81 | + const showsPrimaryHeading = hasPrimaryHeading(sourceCode); |
21 | 82 |
|
22 | 83 | return ( |
23 | 84 | <div className="flex flex-1 flex-col items-center bg-gray-50 px-6 py-12"> |
24 | | - <div className="w-full max-w-4xl"> |
25 | | - <header className="mb-8 text-center"> |
26 | | - <h1 className="mb-4 text-5xl font-bold leading-tight text-primary"> |
27 | | - {data.title} |
| 85 | + <article className="w-full max-w-4xl rounded-xl bg-white p-8 shadow-md sm:p-10"> |
| 86 | + {!showsPrimaryHeading && ( |
| 87 | + <h1 className="text-5xl font-bold leading-tight text-primary"> |
| 88 | + {blog.title} |
28 | 89 | </h1> |
| 90 | + )} |
| 91 | + <div className={showsPrimaryHeading ? "mb-6" : "mb-8 mt-4"}> |
29 | 92 | <p className="text-sm italic text-gray-500"> |
30 | | - By {data.author} • {data.date} |
| 93 | + By {blog.author} | {blog.date} |
31 | 94 | </p> |
32 | | - </header> |
33 | | - <article |
34 | | - className="prose prose-lg lg:prose-xl prose-gray mx-auto leading-relaxed text-gray-700" |
35 | | - dangerouslySetInnerHTML={{ __html: contentHtml }} |
36 | | - /> |
37 | | - </div> |
| 95 | + {blog.tags.length > 0 && ( |
| 96 | + <ul className="mt-4 flex flex-wrap gap-2"> |
| 97 | + {blog.tags.map((tag) => ( |
| 98 | + <li |
| 99 | + key={tag} |
| 100 | + className="rounded-full bg-gray-100 px-3 py-1 text-xs font-medium uppercase tracking-wide text-gray-600" |
| 101 | + > |
| 102 | + {tag} |
| 103 | + </li> |
| 104 | + ))} |
| 105 | + </ul> |
| 106 | + )} |
| 107 | + </div> |
| 108 | + <div> |
| 109 | + <MDXContent /> |
| 110 | + </div> |
| 111 | + </article> |
38 | 112 | </div> |
39 | 113 | ); |
40 | 114 | } catch (error) { |
41 | 115 | console.error("Error rendering blog post:", error); |
42 | | - return notFound(); |
| 116 | + notFound(); |
43 | 117 | } |
44 | 118 | }; |
45 | 119 |
|
46 | | -export const generateStaticParams = async () => { |
47 | | - try { |
48 | | - const directoryExists = await fs |
49 | | - .stat(BLOG_DIRECTORY) |
50 | | - .then((stats) => stats.isDirectory()) |
51 | | - .catch(() => false); |
52 | | - |
53 | | - if (!directoryExists) { |
54 | | - console.log( |
55 | | - "No app/blog/posts directory found. Returning empty params...", |
56 | | - ); |
57 | | - return []; |
58 | | - } |
59 | | - |
60 | | - const files = await fs.readdir(BLOG_DIRECTORY); |
61 | | - |
62 | | - const mdFiles = files.filter((filename) => filename.endsWith(".md")); |
63 | | - |
64 | | - const results = await Promise.allSettled( |
65 | | - mdFiles.map(async (filename) => { |
66 | | - try { |
67 | | - const { published } = await getFileMetadata({ |
68 | | - filename, |
69 | | - directory: BLOG_DIRECTORY, |
70 | | - }); |
71 | | - return { filename, published }; |
72 | | - } catch (error) { |
73 | | - console.error(`Skipping ${filename} due to metadata error:`, error); |
74 | | - return { filename, published: false }; |
75 | | - } |
76 | | - }), |
77 | | - ); |
78 | | - |
79 | | - const publishedFiles = results |
80 | | - .filter((result) => result.status === "fulfilled") |
81 | | - .map((result) => result.value); |
82 | | - |
83 | | - return publishedFiles |
84 | | - .filter(({ published }) => published) |
85 | | - .map(({ filename }) => ({ |
86 | | - slug: filename.replace(/\.md$/, ""), |
87 | | - })); |
88 | | - } catch (error) { |
89 | | - console.error("Error generating static params:", error); |
90 | | - return []; |
91 | | - } |
92 | | -}; |
| 120 | +export const generateStaticParams = async (): Promise< |
| 121 | + Array<{ slug: string }> |
| 122 | +> => (await getAllBlogs()).map(({ slug }) => ({ slug })); |
93 | 123 |
|
94 | 124 | export const generateMetadata = async ({ |
95 | 125 | params, |
96 | 126 | }: Params): Promise<Metadata> => { |
97 | 127 | try { |
98 | 128 | const { slug } = await params; |
99 | | - const { data } = await getProcessedMarkdownFile({ |
100 | | - slug, |
101 | | - directory: BLOG_DIRECTORY, |
102 | | - }); |
| 129 | + const blog = await getBlogBySlug(slug); |
103 | 130 |
|
104 | | - return { |
105 | | - title: data.title, |
106 | | - authors: [{ name: data.author }], |
107 | | - }; |
| 131 | + if (!blog) { |
| 132 | + return { |
| 133 | + title: "Blog Post", |
| 134 | + }; |
| 135 | + } |
| 136 | + |
| 137 | + const { metadata } = await loadBlogPage(slug); |
| 138 | + |
| 139 | + return buildBlogPostMetadata({ |
| 140 | + blog, |
| 141 | + pageMetadata: metadata, |
| 142 | + }); |
108 | 143 | } catch (error) { |
109 | 144 | console.error("Error generating metadata:", error); |
110 | 145 | return { |
|
0 commit comments