Go Back
12 June 2024

Posts in Next.js on Cloudflare Pages

I recently rewrote my site to Next.js. Where it's being hosted on Cloudflare Pages using next-on-pages.

For the most part, next-on-pages works incredibly well. However, if you're new to deploying Next.js sites on Cloudflare Pages, it can get tedious.

This article is how I added posts to my Next.js site hosted on Cloudflare Pages.

Markdown on Next.js

The first part is to add support for Markdown files in Next.js. For this, I used next-mdx-remote. It's a library that allows you to render MDX files as React components.

For this, I first created a declaration file called mdx.d.ts in the root of my project:

1declare module "*.mdx" {
2 let MDXComponent: (props) => JSX.Element;
3 export default MDXComponent;
4}

Then, it was onto reconfiguring my next.config.js to output the site as a static site:

1const nextConfig = {
2 /// ....
3 output: "export",
4};

Now, we need to create a way to fetch posts. In this case, we can create a fetch-posts.ts lib file which contains:

  • A way to fetch all posts
  • A way to fetch a single post

To fetch all posts, we can simply use fs to read the posts directory and filter out the .mdx files:

If our post is not published, we can simply return null:

1import matter from "gray-matter";
2import fs from "fs/promises";
3import path from "path";
4import type { Post } from "../types";
5
6export const getPosts = async () => {
7 const posts = await fs.readdir("./src/posts/");
8
9 return Promise.all(
10 posts
11 .filter((file) => path.extname(file) === ".mdx")
12 .map(async (file) => {
13 const filePath = `./src/posts/${file}`;
14
15 const postContent = await fs.readFile(filePath, "utf8");
16
17 const { data, content } = matter(postContent);
18
19 if (data.published === false) {
20 return null;
21 }
22
23 return {
24 title: data.title,
25 slug: data.slug,
26 date: data.date,
27 description: data.description,
28 views: data.views || null,
29 body: content,
30 } as Post;
31 }),
32 );
33};

To fetch a single post, we can use the getPosts function and filter out the post by the slug:

1export async function getPost(slug: string) {
2 const posts = await getPosts();
3 return posts.find((post) => post?.slug === slug);
4}

Rendering Posts

Now that we have a way to fetch posts, we can render them using next-mdx-remote.

I created a markdown-component.tsx file that contains the components for rendering the markdown.

For example, to render a strong tag, I created a Strong component then added it to the exported mdxComponents object:

1function Strong(
2 props: React.DetailedHTMLProps<
3 React.HTMLAttributes<HTMLElement>,
4 HTMLElement
5 >,
6) {
7 const { children, ...rest } = props;
8 return (
9 <b {...rest} className="text-foreground font-semibold">
10 {children}
11 </b>
12 );
13}
14
15
16export const mdxComponents: MDXComponents = {
17 strong: Strong,
18};

Then, I created a post-body.tsx file that contains the PostBody component, this component uses MDXRemote to render the markdown:

1import { MDXRemote } from "next-mdx-remote/rsc";
2
3import remarkGfm from "remark-gfm";
4import rehypeSlug from "rehype-slug";
5import rehypeAutolinkHeadings from "rehype-autolink-headings";
6import remarkA11yEmoji from "@fec/remark-a11y-emoji";
7import remarkToc from "remark-toc";
8import { mdxComponents } from "./markdown-component";
9
10export function PostBody({ children }: { children: string }) {
11 return (
12 <MDXRemote
13 source={children}
14 options={{
15 mdxOptions: {
16 remarkPlugins: [remarkGfm, remarkA11yEmoji, remarkToc],
17 rehypePlugins: [rehypeSlug, rehypeAutolinkHeadings],
18 },
19 }}
20 components={mdxComponents}
21 />
22 );
23}

For the basic configuration, that's all you need to render posts in Next.js, where you pass the body of the post to the PostBody component.

For our posts/[id] route, we use generateStaticParams to generate the routes for each post, as we can't edge render this.

1export async function generateStaticParams() {
2 const posts = await getPosts();
3
4 return posts.map((post) => ({
5 id: post!.slug,
6 }));
7}

For generating metadata, we can use generateMetadata function.

1export async function generateMetadata({ params }: Props): Promise<Metadata> {
2 const { id } = params;
3 const post = await getPost(id);
4
5 if (!post) return notFound();
6
7 return {
8 title: `${post.title} • dromzeh.dev`,
9 description: post.description,
10 metadataBase: new URL("https://dromzeh.dev"),
11 };
12}

Then in the PostPage, we can fetch the post and render it.

1export default async function PostPage({ params: { id } }: Props) {
2 const post = await getPost(id);
3
4 if (!post) return notFound();
5
6 return (
7 <div className="min-h-screen max-w-xl mx-auto flex items-center justify-center">
8 <section className="flex flex-col space-y-4 mt-8 max-w-xl">
9 <PostBody>{post.body}</PostBody>
10 </section>
11 </div>
12 </div>
13 );
14}

Conclusion

You can freely use this to build onto this to add additional entries such as tags.

You can view the full source code for this site on GitHub.