Creating a simple blog with NextJS and Markdown
The first version of this site was built with Gatsby, and I really liked how simple it was to just chuck a markdown file in and magically have a blog post appear. I decided to rebuild in NextJS for no real reason other than I wanted to try it out, and I wanted to recreate the same simplicitly I had with Gatsby.
MDX
One option is to use MDX, which is a pretty nice library which allows you to use JSX in your markdown files. NextJS have a guide on that, but I didn't need anything more than basic markdown so decided to roll my own.
The index page
The first thing you'll need is a page that contains links to all your posts. You may as well create the post page itself at the same time, I created these two files, plus a blank post to get us started
pages/notes/index.tsx pages/notes/[slug].tsx notes/hello-world.md
The contents on the markdown file can be something like this
--- title: Hello World --- # Test
You'll need to define getStaticProps in your index file, which we'll use to pass the titles and slugs of our posts to the page.
type Props = {
posts: { title: string; slug: string }[];
};
export const getStaticProps = (async () => {
const fileNames = await fs.readdir("./notes");
const posts = await Promise.all(
fileNames.map(async (filename) => {
const file = await fs.readFile(`./notes/${filename}`, "utf8");
const result = matter(file);
return {
title: result.data.title,
slug: filename.replace(".md", ""),
};
})
);
return {
props: {
posts,
},
};
}) satisfies GetStaticProps<Props>;
You can then define your page component, using the posts prop to render your titles and link to the posts themselves.
const Notes: React.FC<InferGetStaticPropsType<typeof getStaticProps>> = ({
posts,
}) => {
return (
<>
{posts.map((post) => (
<Link key={post.slug} href={`/notes/${post.slug}`}>
<h1>{post.title}</h1>
</Link>
))}
</>
);
};
The post page
We'll need to define both getStaticProps and getStaticPaths in the post page. The former gives the page the post's content, and the latter tells NextJS what paths to generate at build time.
type Props = {
title: string;
content?: string;
};
export const getStaticProps = (async ({ params }) => {
if (!params) {
return {
props: {
title: "Something's gone wrong",
},
};
}
const file = await fs.readFile(`./notes/${params.slug}.md`);
const result = matter(file);
return {
props: {
title: result.data.title,
content: result.content,
},
};
}) satisfies GetStaticProps<Props, { slug: string }>;
export const getStaticPaths = (async () => {
const fileNames = await fs.readdir("./notes");
return {
paths: fileNames.map((name) => ({
params: {
slug: name.replace(".md", ""),
},
})),
fallback: false,
};
}) satisfies GetStaticPaths;
getStaticProps gets the post's file and then uses gray-matter to parse the metadata and grab the content. In getStaticPaths we just define the paths NextJS needs to generate, which in my case is just the filename of the markdown file minus the extension.
Rendering the markdown
The final thing to do is render the post itself, for this I used the fantastic react-markdown. The thing I like about this library is that you can define components to render for specific HTML tags, which is great if you want to customise your styling. I created a wrapper around this
const Markdown: React.FC<Props> = ({ children }) => { return ( <MarkdownRenderer components={{ h1: H1, p: P, code: Code, a: A }}> {children} </MarkdownRenderer> ); };
You can then define your components, for example I use react-syntax-highlighter to render code
const Code: Components["code"] = (props) => { const language = props.className ? props.className.replace("language-", "") : ""; return ( <SyntaxHighlighter style={stackoverflowLight} language={language} PreTag={Pre} > {String(props.children).replace(/\n$/, "")} </SyntaxHighlighter> ); };