Build a NextJS Blog with MDX and Tailwind.

Hello programmers,

Do you want start your blog where you educate others, or may be you want a blog as a repository of all the information you’ve gathered over the years. Anyway, blogs can be a great source of information providers for others as well as yourself. It can really help you connect deep with the content you want to consume. Setting up a blog is easy, especially if you’re a programmer. You can create your own blog with Next.JS and MDX. In this article, I will show you exactly how to do that!

What are we going to build?

By the end of this article, we will have a blog site for ourselves, which is going to look like this. You can off course make it look more beautiful, but for the sake of tutorial, I made it look very simple.

Introduction

What do we need?

  • A decent knowledge of Next.JS framework
  • Dependencies - path fs gray-matter next-mdx-remote
  • Tailwind CSS

Let’s Start!

Create a next project

First of al, we’ll start by creating a next project

yarn create next-app blog

cd blog

Install all the necessary dependencies.

yarn add fs path gray-matter next-mdx-remote

fs Provides a way to work with files
path Provides a way to work with directories and paths.
gray-matter Parses the front-matter from a string or file
next-mdx-remote To render your mdx content on the page

Setting up Tailwind

Run the following commands, in your terminal to install tailwind.

yarn add tailwindcss postcss autoprefixer -D

Run this command to create a tailwind.config.js file

npx tailwindcss init -p

Inside the tailwind.config.js, paste the following

// tailwind.config.js
module.exports = { 
mode: "jit",
content: [   
    "./pages/**/*.{js,ts,jsx,tsx}", 
    "./components/**/*.{js,ts,jsx,tsx}",  
],  
theme: {  
    extend: {}, 
},
plugins: [],
}

Include these in your styles/globals.css file

/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

Homepage

In index.js file, create an async function getStaticProps(). getStaticProps() is used in data fetching and returning the result as a prop to the same component. Next.JS will render this page at build time.

// pages/index.js

export async function getStaticProps() {
  // Read the pages/posts dir
  let files = fs.readdirSync(path.join("pages/posts"));

  // Get only the mdx files
  files = files.filter((file) => file.split(".")[1] === "mdx");

  // Read each file and extract front matter
  const posts = await Promise.all(
    files.map((file) => {
      const mdWithData = fs.readFileSync(
        path.join("pages/posts", file),
        "utf-8"
      );

      const { data: frontMatter } = matter(mdWithData);

      return {
        frontMatter,
        slug: file.split(".")[0],
      };
    })
  );

  // Return all the posts frontMatter and slug as props
  return {
    props: {
      posts,
    },
  };
}

Inside getStaticProps we will use the fs and path module to read the .mdx stored inside the /pages/posts directory.

We will then filter the result to only get the MDX files and not the [slug.js] file that we will create ahead.

files = files.filter((file) => file.split(".")[1] === "mdx");

We will then map through each file using the .map array function and then read each individual file using the fs and path module and extract the front matter of the file using the matter() function (imported from gray-matter) and store the front matter along with slug of every file in the posts variable.

// import matter from 'gray-matter';

// Read each file and extract front matter
  const posts = await Promise.all(
    files.map((file) => {
            // read file
      const mdWithData = fs.readFileSync(
        path.join("pages/posts", file),
        "utf-8"
      );

            // extract front matter
      const { data: frontMatter } = matter(mdWithData);

      return {
        frontMatter,
        slug: file.split(".")[0],
      };
    })
  );

posts variable will look somethings like this -

posts = {
    frontMatter: {
        // frontMatter object extracted from the mdx file
    },
    slug: string
}[]

At last, we will map through each post (inside the props) and render it in the UI. We will also use the Link component from next to create a link to each post.

The final index.js file will look like this

// pages/index.js
import fs from "fs";
import path from "path";
import matter from "gray-matter";
import Link from "next/link";
import PostCard from "../components/PostCard";
import Layout from "../components/Layout";

const Home = ({ posts }) => {
  return (
      <div className="container w-[80%] md:w-[60%] mx-auto">
        <h1 className="text-blue-700 text-3xl font-bold my-12">My Blog 📙</h1>
        <div className="posts md:grid md:grid-cols-3 gap-8">
          {posts.map((post) => (
            <Link href={`/posts/${post.slug}`} key={post.slug}>
              <a>
                <PostCard post={post} />
              </a>
            </Link>
          ))}
        </div>
      </div>
  );
};

export default Home;

export async function getStaticProps() {
  // Read the pages/posts dir
  let files = fs.readdirSync(path.join("pages/posts"));

  // Get only the mdx files
  files = files.filter((file) => file.split(".")[1] === "mdx");

  // Read each file and extract front matter
  const posts = await Promise.all(
    files.map((file) => {
      const mdWithData = fs.readFileSync(
        path.join("pages/posts", file),
        "utf-8"
      );

      const { data: frontMatter } = matter(mdWithData);

      return {
        frontMatter,
        slug: file.split(".")[0],
      };
    })
  );

  // Return all the posts frontMatter and slug as props
  return {
    props: {
      posts,
    },
  };
}

PostCard Component

Create a component components/PostCard.js. We will use this component to return card for each post.

const PostCard = ({ post }) => {
  return (
    <div className="rounded-md w-72 border transition-all hover:text-blue-700 hover:shadow-lg hover-scale:105 cursor-pointer">
      <img src={post.frontMatter.cover_image} alt="Cover Image" />
      <div className="mt-2 p-2">
        <h2 className="font-semibold text-xl">{post.frontMatter.title}</h2>
      </div>
    </div>
  );
};

export default PostCard;

Post page

Create a /pages/posts/[slug].js page to render each post separately on a different route.

We will use the getStaticPaths async function to generate separate routes according to the slug for each post at the build time.

export async function getStaticPaths() {
  // Read the files inside the pages/posts dir
  const files = fs.readdirSync(path.join("pages/posts"));

  // Generate path for each file
  const paths = files.map((file) => {
    return {
      params: {
        slug: file.replace(".mdx", ""),
      },
    };
  });

  return {
    paths,
    fallback: false,
  };
}

We will the getStaticProps once again to read files and extract front matter as well as the content from it using the gray-matter module. The content of the mdx files need to be serailized in order to render it using the next-mdx-remote module.

export async function getStaticProps({ params: { slug } }) {
  // read each file
  const markdown = fs.readFileSync(
    path.join("pages/posts", slug + ".mdx"),
    "utf-8"
  );

  // Extract front matter
  const { data: frontMatter, content } = matter(markdown);

  const mdxSource = await serialize(content);

  return {
    props: {
      frontMatter,
      slug,
      mdxSource,
    },
  };
}

We wil then render the mdx source recieved inside the props.

// pages/posts/[slug.js]
import path from "path";
import matter from "gray-matter";
import { serialize } from "next-mdx-remote/serialize";
import { MDXRemote } from "next-mdx-remote";
import styles from "../../styles/Post.module.css";

const Post = ({ frontMatter, slug, mdxSource }) => {
   return (
    <Layout title={frontMatter.title}>
      <div className={styles.post}>
        <h1 className="font-semibold my-8 text-3xl text-blue-700">
          {frontMatter.title}
        </h1>
        <MDXRemote {...mdxSource} />
      </div>
    </Layout>
  );
};

Styling Post page

We will also add some basic styling for the post page using tailwind directives. Create a styles/Post.module.css file and include these styles for a better look.

// styles/Post.module.css

.post {
  @apply container w-[90%] md:w-[60%] mx-auto my-12;
}

.post p {
  @apply leading-7 my-4;
}

.post img {
  @apply my-4 w-full;
}

Bonus - Syntax Highlighting! 🎉

If you want to see, how to add syntax highlighting for your code elements in the mdx files, you can checkout my full video tutorial I did on my YouTube channel

Connect with me

Twitter - shaancodes
Github - shaan-alam
YouTube - shaancodes
Instgram - shaancodes

19