Markdown is a simple syntax for formatting text files. If you’re used to GitHub, you’re probably familiar with creating and editing files in your repos. MDX is an extension of markdown that adds support for JSX. Using Next.js’ Static Site generation, we can create a very simple to use, fast and light blog, with markdown to store our content.


The first thing we’ll need is to set up our Next.js project.

Here’s what we want our site to look like in the end:

A very straightforward blog layout, with a home page to select an article, and some styling to render our blog content.

I’m storing my blog posts as MDX files in a directory in my project:

This is just to keep things simple, but they could be loaded in from an API call, or any source.

Helper Functions

I have a few helper functions to help with things like finding all of my blog posts, extracting the filename from them, or finding the path from a post name. You can check out the code for this project on GitHub, but here’s the functions I’ll be using:

function getSlug(slugPath: string) {
    const [slug] = /.+(?=.mdx)/i.exec(path.basename(slugPath)) as string[];
    return slug;

getSlug() uses the path API and some Regex to get the filename, or post slug from a path like “posts/my-post.mdx”

export function findAllPostSlugs() {
    return glob(path.join(BLOG_PATH, '*.mdx')).then((paths) =>

findAllPostSlugs() uses the library glob to find all the .mdx files in my post directory, and then extract the slugs from them into an array.


The first step for our site is loading in the markdown files. We’re going to use Static Site Generation for this. You could also use an approach like SSR, or just loading the markdown files in the client, but something like blog posts, where the content won’t update often, is a perfect use case for SSG.

SSG in Next.js works through two functions. The first is getStaticPaths(). Inside this function, you generate a list of all the possible versions of the page you want Next.js to generate. For a blog, this is a page for each post we can find.

export const getStaticPaths: GetStaticPaths = async () => {
    const slugs = await findAllPostSlugs();
    return {
        paths: => {
            return { params: { slug } };
        fallback: false, // true, false or 'blocking'

We’re using our findAllPostSlugs() function from earlier to get our list of all the blog post slugs. Then Next.js expects us to return an object containing all of the possible paths for each blog post.

The fallback option here decides whether or not we want Next.js to show a 404 page if we try to navigate to a path not found in paths, or to serve the same page. In a situation like this you could select either option. You could set it to false and return a 404 page, since the blog post doesn’t exist, or set it to true send it to the same page, and handle the blog post not being found inside the page, e.g. showing an error, or a list of posts instead.

For each page, our params object then gets sent to getStaticProps():

export const getStaticProps: GetStaticProps = async ({ params }) => {
    const { content, data } = await loadMdxFromSlug(params?.slug as string);
    const mdxSource = await serialize(content, {
        // Optionally pass remark/rehype plugins
        mdxOptions: {
            remarkPlugins: [],
            rehypePlugins: [],
        scope: data,
    return {
        props: {
            source: mdxSource,
            frontMatter: data,

Inside here we actually load our .mdx file, and use next-mdx-remote to process it. Part of this processing involves separating it into the content, and the front matter; metadata about our content.

Blog Page

Here’s the page for our blog post component:

export default function BlogPost({ source, frontMatter }: BlogPostProps) {
    return (
        <div className="flex min-h-screen w-full items-center justify-center dark:bg-stone-900">
            <div className="prose prose-invert h-full w-full rounded border border-stone-700 p-10 shadow dark:bg-stone-800">
                <MDXRemote {...source} />

We pass our processed mdx to MDXRemote, which handles the rendering. The frontMatter is just a simple object containing our metadata, it looks like this:

One key point about next-mdx-remote is that while it handles converting your markdown into HTML, it doesn’t offer any styling. This is where TailwindCSS steps in, the TailwindCSS Typography plugin gives you a set of default styles for making your content look good. You can use alternate style systems, but TailwindCSS is a great solution for this.


Our homepage uses a similar approach with static site generation. The main difference is since there’s only one version of our page, we don’t need to use getStaticPaths().

export const getStaticProps: GetStaticProps = async () => {
    // MDX text - can be from a local file, database, anywhere
    const allSlugs = await findAllPostSlugs();
    const allSources = await Promise.all( (slug) => {
            const source = await loadMdxFromSlug(slug);
            return { slug, source };

    //We only want the slug and the frontmatter
    const posts ={ slug, source }) => {
        return { slug, data: };

    return {
        props: {

This is a similar approach to the blog post page, the whole process is:

  • Get a list of all the post slugs
  • Load the markdown file for each slug
  • We don’t need the content for each slug, so we use a map to just extract the slug for each post, and pass on the frontmatter for the title and description.

Then inside our page component, we have a simple list of links to each blog post.

And here’s our end result:

Thanks for reading !f you’re looking to take this project further, GitHub pages can be a great free option for deploying your blog. Check out the full project here, and if you liked this article, feel free to leave a comment below.

Avatar photo
👋 Hey, I'm Omari Thompson-Edwards
Hey, I'm Omari! I'm a full-stack developer from the UK. I'm currently looking for graduate and freelance software engineering roles, so if you liked this article, reach out on Twitter at @marile0n

💬 Leave a comment

Your email address will not be published. Required fields are marked *

We will never share your email with anyone else.