How I built my blog with Next.js, TypeScript, MDX, Tailwind

February 22, 2022

(updated July 23, 2022)

Frontend

TLDR: In this article, we briefly go over why everyone needs their own blog, their own podcast, their own TV show and their own biography. Then we go over how I built this blog with Next.js, TypeScript, MDX, Tailwind.

This article and this blog are inspired by Josh W. Comeau's article on how he built his blog - his article is a little less of a step-by-step guide but goes over some of the fancy goodies he has built on his blog. It is an interesting read.

Why build your own blog?

First of all, let's address why you would want to have a blog. One of the reasons is that if you are anything like me and aspire to one day become President or some other kind of important person, you absolutely need a blog.

Having a blog will give you a place where you can write about the billion dollar ideas you have and where you can share your favorite Gary Vee and Steve Jobs quotes. It will also impress your mom, and that too is a good thing.

Now, on to how you should build it. There are about many frameworks and SaaS offerings you could use to build your own blog. A lot of startups out there use Ghost. Some also use Webflow. There are a billion more. There also are more programmer-oriented frameworks you could use too. There's Gatsby. There's Hugo. Plenty of options out there.

But here's the thing, what is good enough for companies like Buffer or even Duolingo is sure to not be good enough for your personal blog.

Let's get started, reinvent the wheel together and impress your mom and your friends (if they exist).

Frameworks and Libraries

Though I enjoy reinventing the wheel, I also enjoy running npm install until my node_modules lootcrate weighs 50TB.

Here is a little list of the basic dependencies we are going to be working with:

Next.js is a meta-framework that lets you build isomorphic apps. What that complex word that erudites such as myself know means is that Next.js is a framework on top of the React framework and lets you do fancy things such as Server-Side Rendering (SSR) and Static-Side Generation (SSG). Here's an article that explains what that is all about.

The toolchain can get a little messy when working with React and TypeScript. Fortunately for you, if you are starting your project from scratch, you can use create-next-app to generate your Next.js project in TypeScript directly and it all works out of the box without having to mess with a bunch of configuration files.

To make the website look epic, we are going to be using Tailwind. If you are familiar with Bootstrap, Tailwind is a little similar. The difference is that Tailwind is very close to CSS properties and is not ancient tech akin to the sundial.

The way Tailwind works is it gives you CSS classes like bg-blue-500 that add corresponding CSS like background-color: "#3b82f6";. It has classes for everything, some of them map directly to CSS - e.g. Tailwind has a flex class for example - and others are high-level ones, for example, that bg-{color}-{strength} class I just mentioned which maps to different colors from the Tailwind color palette.

As for the blog post content itself, I write the posts in MDX. MDX is like Markdown except you can embed JSX components in it. You write your MDX, write React code here and there, and then you parse all that text (using one of the many available libraries for that) and you can use the result in your React code.

In MDX, you can also modify how the library converts the MDX elements into React - for example, you could override the way it behaves for large titles and have it render a custom component instead of a plain <h1></h1>.

Here's what MDX looks like:

MDX
# Some page title

You can use [hyperlinks](https://mdxjs.com/) as with normal Markdown.

- some list item
- some other list item

You can embed components and write HTML:

<div className="bg-blue-500">
  <SomeComponent someProp={8} />
</div>

There are a couple of reasons why you might want to use MDX instead of writing a different NextPage for each blog post you write. The main one probably being that it lets you truly decouple the content from the more visual aspects of the website.

Using MDX to write your articles

There are several packages you could use to integrate MDX content into your Next.js app:

I use next-mdx-remote because it seemed like the easiest to get started with quickly, and I am a busy man (yes). Knowing what I now know, I might have gone with mdx-bundler instead because as its name suggests, it can handle bundling, which would be a nice bonus.

Where to store your MDX

There are a few options when it comes to storing your MDX content. You could stash that MDX content away into a database and pull it from there at runtime or build time (with Next.js's getStaticProps).

Since we are talking about a blog that will impress your mom here, you can do what I do, which is keep your MDX files in a directory called /posts or /articles (or whatever) at the root of your Next.js project. This comes with the nice bonus that you can commit your articles to GitHub.

.
├── ...
├── components
│ └── ...
├── pages
│ └── ...
└── posts
  └── how-to-build-your-own-blog-with-nextjs.mdx

Now, if you have a 5,000+ IQ, you might be wondering how I handle metadata for each blog post. For example, with that how-to-build-your-own-blog-with-nextjs.mdx file, where do I put the metadata such as title, tags, date, thumbnail and other such things.

I store the metadata in a format called YAML Front Matter. That is a YAML-esque format that you can use to write arbitrary structured data at the top of MDX or Markdown files.

You can define whatever key-value in Front Matter. We'll see how to parse that Front Matter header later on in the article.

Here's an example of what the Front Matter data looks like:

MDX
---
title: Building a Blog with Next.js
description: You can build your own blog with Next.js...
date: 2022-02-22T09:15:00-0400
tags: Frontend, React
thumbnail: /img/some-thumbnail.png
---

## Why build your own blog?

If you are anything like me and aspire to one day become President
or some other kind of big wig, you probably need a blog...

[...]

Here's the answer: I store the metadata in a format called
**YAML Front Matter**. That is a YAML-esque format that you
can use to define some structured data at the top of your
MDX (or regular Markdown) files.

Here's an example:

🤯 🤯 🤯 OMG! Recursion! Maximum Call Stack Size Exceeded!! 🤯 🤯 🤯

Static generation of the articles (prerendering)

Now that you've got your MDX files in that /posts directory, you need to create a NextPage that will dynamically render each post.

The way dynamic routes work in Next.js is through naming of the files you put in the /pages directory.

If you have /pages/posts/[slug].tsx, that will create a dynamic route with [slug] as a path parameter. In this case, the route would be https://example.com/posts/[slug]. Whatever concrete value you pass as that parameter will then be passed on to your NextPage. For example, if somebody loads https://example.com/posts/hello-there, the hello-there will be made accessible to your /pages/posts/[slug].tsx component.

Now, since all that MDX blog content is static - which means it is the same on every request regardless of the user and anything else - we can prerender the dynamic pages at build time.

To do that, we need to use two special Next.js functions. The first one is called getStaticPaths, which generates every possible value that the route parameter can take on (that is the parameter [slug] in /posts/[slug].tsx) and it is run at build time.

The second special function is called getStaticProps, which passes props to the NextPage at build time. Because you pass in the props at build time and know what the page is going to be like, the page can be prerendered instead of being needlessly rerendered on every request in spite of never changing.

Here's some pseudocode to give you a sense of how you might want to use getStaticPaths to prerender your MDX blog posts:

TSX
/pages/posts/[slug].tsx
interface StaticSlugPath {
  params: {
    slug: string
  }
}

export const getStaticPaths: GetStaticPaths = async () => {

  // get all your mdx files
  // e.g. ['some-article.mdx', 'some-other-article.mdx']
  const filenames: string[] = fs.readdirSync(postsDirectory);

  // let's use the filenames without `.mdx` extension as our slugs
  // e.g.
  // [
  //   { params: { slug: 'some-article' } },
  //   { params: { slug: 'some-other-article' } }
  // ]
  const slugs: StaticSlugPath[] = filenames.map((filename) => {
    return {
      params: {
        slug: filename.replace(/.mdx$/, ""),
      },
    };
  });

  // the stuff we return here will be passed to `getStaticProps`
  return {
    slugs,
    fallback: false
  };
};

Now Next.js knows all the values that [slug] can take on and it will use that information to know what to prerender.

Then we need to use another special Next.js function named getStaticProps, which will, given a slug, parse the corresponding .mdx file content and pass it as a prop to the Page component. All this happens at build time.

With getStaticProps, you use the slugs you got from getStaticPaths to generate the props you want to pass to your NextPage during the static generation:

TSX
/pages/posts/[slug].tsx
export const getStaticProps: GetStaticProps = async (context) => {
  // e.g. 'some-article'; this is passed by the previous step
  // in getStaticPaths
  const slug: string = context.params.slug

  // let's read the content of the `.mdx` file into a string
  const data: string = readFileContent(slug);

  // let's now parse the MDX string into an object
  const source: MDXRemoteSerializeResult = await serialize(data, {

    // remember that YAML Front Matter from earlier?
    // well this parameter will parse it
    parseFrontmatter: true,

    // you can pass some plugins here
    // rehypeHighlight does code highlighting for example
    mdxOptions: {
      rehypePlugins: [rehypeHighlight]
    },
  });

  // the stuff we return here will be passed to `NextPage`
  return { props: { source } };
};

Then we can use the props we returned from getStaticProps inside our NextPage:

TSX
/pages/posts/[slug].tsx
interface PostPageProps {
  // the `source` is what is being passed by getStaticProps
  source: MDXRemoteSerializeResult;
}

const PostPage: NextPage<PageProps> = ({ source }) => {
  return (
    <Layout>
      <div>
        <div>
          <h1>{source.frontmatter.title}</h1>
          <p>
            <FormattedDate date={source.frontmatter.date} />
          </p>
        </div>

        ...

        <div>
          <MDXRemote
            {...source}

            // pass your custom components and component
            // mappings (https://github.com/hashicorp/next-mdx-remote#replacing-default-components)
            components={customComponents}
          />
        </div>
      </div>
    </Layout>
  );
};

That's it, now whenever you run yarn build or npm run build, Next.js will prerender the PostPage pages based on the MDX files you have in your /posts directory.

If this is still a little confusing to you, here's basically what's going on - this is an example in pseudocode of the whole build process, but it is obviously not actual code:

Pseudocode
// what happens when we run `yarn build` or `npm run build`
const build = () => {
  // get all the possible paths the dynamic route can take on
  const paths = getStaticPaths()

  // go through each possible path to prerender the content
  // for it
  for (const path of paths) {
    // get the static props corresponding to that path
    const props = getStaticProps(path)

    // prerender the page with the props we just got
    const page = page(props)
  }
}

Index page

We are done building the dynamic page that renders individual posts. That was already high-tech but buckle up because we are not done yet. We need some sort of page that lists all of our posts and lets us click on whichever one we want to read - at least if we want our blog to be usable. Let's call that an index page.

Here's what we can do here. Using getStaticPaths, we can read all the MDX files in the /posts directory, parse the YAML Front Matter metadata and returns an array of all the metadata (and each article slug) to our new NextPage (the index page that lists all of our posts):

TSX
/pages/index.tsx
interface Meta {
  slug: string
  title: string
  date: Date
  tags: string[]
}

export const getStaticProps: GetStaticProps = async () => {

  // e.g. ['some-article.mdx', 'some-other-article.mdx']
  const filenames: string[] = fs.readdirSync(postsDirectory);

  const metadata: Meta[] = filenames.map((filename) => {
    // you can use "gray-matter" to parse the YAML Front Matter
    const meta = parseFrontMatter(filename)

    // remove the `.mdx` extension to only keep the slug
    const slug = filename.replace(/.mdx$/, "")
    return {
      ...meta,
      slug,
    }
  })

  // pass that metadata to the NextPage at build time
  return { props: { metadata } };
};

The NextPage then receives all that data and renders each post overview:

TSX
/components/posts/PostCard.tsx
interface PostCardProps {
  slug: string
  title: string
  date: Date
  tags: string[]
}

// build the URL to the post based on the slug
// this is the URL for your dynamic route from
// earlier
function buildPostUrl(slug: string): string {
  return "/posts" + slug
}

const PostCard: React.FC<PostCardProps> = ({ title, date, tags, slug }) {
  const url = buildPostUrl(slug)
  return (
    <div>
      <a href={url}>
        <h2>{title}</h2>
        <div>{date}</div>
        <div>...</div>
      </a>
    </div>
  )
}

const Home: NextPage<PageProps> = ({ metadata }) => {
  return (
    <Layout>
      <div>
        <div>
          <h1>Welcome!</h1>
        </div>
        <div>
          {metadata.map((meta) => (
              <PostCard key={meta.slug} {...meta} />
            )
          )}
        </div>
      </div>
    </Layout>
  );
};

Running yarn build or npm run build will now also generate your index page. Now if you want to add some fancier page that can display posts by tag or some other page of the sort, you should be able to do so by simply modifying the logic inside getStaticProps.

My Posts page works this way. In getStaticProps, I group posts by tags and then iterate over the groups to render each article for each tag.

As you might expect, we would need to rework that logic a bit if we needed pagination.

But for now, that's good enough to impress your mom, so I guess we are done!

If you want to see a repository for this - maybe an unstyled version of this blog with everything hooked up, like a basic framework type of thing - do let me know on Twitter or in the comments and I'll consider making one if enough people find it useful.