loading...

Making MDX blog with Next.js - Part 1

jashnm profile image Jashn Maloo Originally published at jashn.xyz Updated on ・8 min read

When I thought of making a blog with Next.js, the first technology that came in my mind was MDX, because why not? With Next.js, it's a superpower with features like -

  • It is treated just like any other page
  • One can insert react based components
  • Can be imported as components to other files
  • Super fast. There's no runtime.
  • Being markdown based, it's simple and very flexible to use

What about SEO ?
Well, it's supported as well, add front-matter to it, parse it with gray matter or similar library and you're good to go.
(I use next-seo for SEO in Next.js apps, but it won't be covered in this series. You can watch this amazing tutorial for next-seo.)

Prerequisites

You should be well versed with basics of React. Knowledge of Next.js is a plus, but I will be discussing some foundations of Next.js.

So What's the process ?

I won't say it's super easy because I've spent days trying several ways to implement all the above discussed points. But once you know it, it's a cheesecake.
Of the problems generally faced with SEO, components, parsing, I'll be discussing them as we move forward. Rest assured, it'll be a complete tutorial for building your own MDX blog.

Considering that I'll go in as much details as I can, it will be 2 part series. First one will definitely be long, but easy. We'll do the basic setup, with theming, essential pages, functions will be created in this post,
while the next one will cover dynamic routing, handling components, front-matter in the post.

Repo: mdx-blog-nextjs

1. Initializing project

Bootstrap Next.js project with npx create-next-app and provide name to your project. In this case I'll be calling my project mdx-blog.
After it is done, cd into the project directory(our root directory).

This is how a typical Next.js folder structure looks like -

folder structure

pages folder is the one where all the routes respond to in Next.js, so that's really important.

2. Adding styles & dependencies

Let's install most of the dependencies now. I'll discuss about these as we start using these. You can use yarn or npm according to your choice, I'm going with npm.

npm i theme-ui @theme-ui/presets gray-matter @next/mdx
Enter fullscreen mode Exit fullscreen mode

For this project, I'm using theme-ui as my UI library and for syntax highlighting in thee blog, you can go with whatever you like, Chakra UI or Material UI or any other.
So We're installing theme-ui and its other packages with gray-matter for extracting SEO details from MDX files, next-seo for inserting meta data in our pages and @next/mdx for MDX files support in Next.js

Now let's create theme.js file in root directory to provide a theme preset and basic styles, and add the following to it -

// ./theme.js

import { roboto } from "@theme-ui/presets";

const theme = {
  ...roboto,

  containers: {
    page: {
      width: "98%",
      maxWidth: "840px",
      m: 0,
      mx: "auto",
      justifyContent: "center",
      height: "100%",
      py: "1rem",
      px: ["0.4rem", "2rem"],
    },

    postCard: {
      p: "2",
      borderRadius: "4px",
      border: "1px solid #eeeeee",
      width: "100%",
    },
  },

  text: {
    heading: {
      py: "0.4rem",
    },
  },
};

export default theme;
Enter fullscreen mode Exit fullscreen mode

It contains basic styling enough to build a decent looking blog, rest we'll be doing inside our pages.

Okay, theme done. Now we goota make our _app.js file ready, it is a supereme file, just like index.js in CRA.
Many UI libraries require ThemeProvider at the global level, so let's add it in _app.js

// ./_app.js

import "../styles/globals.css";
import { ThemeProvider } from "theme-ui";
import theme from "../theme";

function MyApp({ Component, pageProps }) {
  return (
    <ThemeProvider theme={theme}>
      <Component {...pageProps} />
    </ThemeProvider>
  );
}

export default MyApp;
Enter fullscreen mode Exit fullscreen mode

Once done, make Next.js config file next.config.js in the root directory. In next.config.js, add the following code

// ./next.config.js

const withMDX = require("@next/mdx")();

module.exports = withMDX({
  pageExtensions: ["js", "mdx"],
});
Enter fullscreen mode Exit fullscreen mode

This config file is required to tell Next.js that we're using MDX files, so it should treat them as regular pages.
After this, you can literally start building pages in MDX in the pages folder and you'll be ablle to see them in your browser. But we're building a blog, so there are more things involved.

3. Writing first MDX file

So we're ready to make MDX pages, make a folder named posts in the root directory and make two MDX files, then add the following content

// ./posts/getting-started-with-mdx.mdx
---
title: "Getting started with MDX"
date: "2020-10-10"
author: "MDX Team"
excerpt: "Making this in MDX for simplicity and flexibility"
---

## Why MDX ?

❤️ **Powerful**: MDX blends markdown and JSX syntax to fit perfectly in
React/JSX-based projects.

💻 **Everything is a component**: Use existing components inside your
MDX and import other MDX files as plain components.

🔧 **Customizable**: Decide which component is rendered for each markdown
element (`{ h1: MyHeading }`).

📚 **Markdown-based**: The simplicity and elegance of markdown remains,
you interleave JSX only when you want to.

🔥 **Blazingly blazing fast**: MDX has no runtime, all compilation occurs
during the build stage.

[MDX](https://mdxjs.com) is markdown for component era.

Enter fullscreen mode Exit fullscreen mode
// ./posts/some-random-points.mdx

---
title: "Writing random points"
date: "2020-10-09"
author: "Jashn Maloo"
excerpt: "World is random in what it does, so let's write something random"
---


## Some random points

- Isn't writing in markdown lovely?
- Aren't components so reusable?
- Why is Next.js so fantastic?
- Please give me 2021.

### Some random heading


Enter fullscreen mode Exit fullscreen mode

Cool, but it's not yet ready to be shown on our site. Because it's outside our pages directory, first we'll make index page for all the posts we'll have and slug out of these file.

Make a lib folder in your root directory. A lib folder generally contains all that code that is needed to get information in pages to run properly.
In the lib folder, make a file named posts.js and add the following code to it

// ./lib/posts.js

import fs from "fs";
import path from "path";
import matter from "gray-matter";

//Finding directory named "posts" from the current working directory of Node.
const postDirectory = path.join(process.cwd(), "posts");

export const getSortedPosts = () => {
  //Reads all the files in the post directory
  const fileNames = fs.readdirSync(postDirectory);

  const allPostsData = fileNames.map((filename) => {
    const slug = filename.replace(".mdx", "");

    const fullPath = path.join(postDirectory, filename);
    //Extracts contents of the MDX file
    const fileContents = fs.readFileSync(fullPath, "utf8");
    const { data } = matter(fileContents);

    const options = { month: "long", day: "numeric", year: "numeric" };
    const formattedDate = new Date(data.date).toLocaleDateString(
      "en-IN",
      options
    );

    const frontmatter = {
      ...data,
      date: formattedDate,
    };
    return {
      slug,
      ...frontmatter,
    };
  });

  return allPostsData.sort((a, b) => {
    if (new Date(a.date) < new Date(b.date)) {
      return 1;
    } else {
      return -1;
    }
  });
};
Enter fullscreen mode Exit fullscreen mode

Okay so it's a good amount of code, wait until we add more to it xD, but let's understand what happened here.

First, we fetched the directory named posts from our root. Then with the help of node's fs package we read all the contents in the posts folder.
allPostData function is doing the chunk of work here -

  1. First we made slug for each file in posts directory
  2. We fetched the content of each file by joining the directory and filename.
  3. Contents from front matter is fetched in data
  4. We changed the date format to be visible on the index page. You can totally skip this part, it's not necessary.
  5. We updated our frontmatter and returned slug and frontmatter to allPostData function
  6. If you want your posts to be sorted in some way in index file, that's what we're doing next. Latest to oldest here.

Woof! So much here, but it's very much same to what you can find in Next's docs or some other articles.
Now we're ready to show the posts on our index page, so let's create one.

4. Creating Index of all posts

I want my blog posts route to be /blog/post/, so make a folder named blog in the pages directory and inside that, create a index.js file. Now when we'll start
our app /blog route will point to this index file.

I'll now share the code, but beware if it's your first time working with dynamic pages in Next.js, high chances that few lines will go over your head, but it's alright, I'll describe everything that's happeneing.
So, add the below content to your ./blog/index.js/ file.

// ./blog/index.js

/** @jsx jsx */
import { jsx, Flex, Heading, Box, Text } from "theme-ui";
import Link from "next/link";
import { getSortedPosts } from "../../lib/posts";

const BlogIndex = ({ allPostsData }) => {
  return (
    <>
      <Box sx={{ variant: "containers.page" }}>
        <Heading>My Blog</Heading>
        <Flex
          sx={{
            flexWrap: "wrap",
            mt: "2rem",
            direction: "column",
          }}
        >
          {allPostsData.map(({ slug, date, title, excerpt }) => (
            <Box variant="containers.postCard" sx={{ my: "0.5rem" }} key={slug}>
              <li
                sx={{
                  display: "flex",
                  flexDirection: ["column", "row"],
                  my: "1rem",
                }}
              >
                <Box>
                  <Link key={slug} href="/blog/[slug]" as={`/blog/${slug}`}>
                    <a>
                      <Heading
                        sx={{
                          fontSize: "calc(1.6rem + 0.2vw)",
                          fontWeight: "500",
                        }}
                      >
                        {title}
                      </Heading>
                    </a>
                  </Link>

                  <Box sx={{ my: "0.5rem" }}>{excerpt}</Box>

                  <Text>{date}</Text>
                </Box>
              </li>
            </Box>
          ))}
        </Flex>
      </Box>
    </>
  );
};
export default BlogIndex;

export async function getStaticProps() {
  const allPostsData = getSortedPosts();
  return {
    props: {
      allPostsData,
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

So it's just the layout of the index page? How are we fetching posts here?

I've got the answers, don't worry. It's more than just the layout of index page.
First, if you're curious what are those variants or sx or that jsx I wrote on the top, they all are related to theme-ui and variants is how we connect that theme.js file we made earlier with the concerned component,
so if you're using some other UI library, you don't need to import these things.

So about fetching posts, we're importing getSortedPosts from posts.js we created in lib folder.
What this function will do is, it'll store all the posts in allPostsData, which then can be returned to the page above(BlogIndex here) just like props in a react component.

Now if you're new to Next.js, you might be asking what is this getStaticProps and what role is it playing here?
So in version 9.3, Next.js included getStaticProps and getServerSideProps for fetching data in the page without using any other library. And if you compare the names
you might get an idea of what do they do. getStaticProps fetches all the data at build time, making our website static, just like Gatsby. While getServerSideProps fetches data on runtime, just like many other server rendered websites.
Although both are SEO friendly, getStaticProps being static really adds up to the website performance. It is the one returning props to the page above.

Ah! so much of getStatic, getServerSide, right? Well, people like these.

Now I guess the last thing bothering you might be, "what's with the Link component, why it's being so weird?".
So as I said earlier, I want by posts to be on route /blog/post-name. We'll have a lot of posts as time will pass and we can't just make many such index components for each file, so we need dynamic routing here and that's how it is handled in Next.js.
First we told href that the link will be of style /blog/[slug] so it knows it's dynamic, and then we're telling as prop what the actual value is.

Now it's still not clear, don't worry, once we will make the page for showing full post, it'll be more clear.

For now, let's run our code to see how it looks. In your terminal, make sure you're in the directory of your project, the run this

npm run dev

Enter fullscreen mode Exit fullscreen mode

This will start next server on post 3000. And once you go to http://localhost:3000/blog, if you see something like the image below, you're going good

Index page for posts

And if there are some troubles, maybe just contact me and I'll try my best to make it work for you.


So that's the end of part-1 of Making MDX blog with Next.js. In the next and final part, we'll discuss some common issues of handling components, front-matter and MDX, while completing this blog.

Discussion

pic
Editor guide