DEV Community

Cover image for Build a NextJS Blog with MDX and Tailwind.
Shaan Alam
Shaan Alam

Posted on

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.

Blog

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: [],
}
Enter fullscreen mode Exit fullscreen mode

Include these in your styles/globals.css file

/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

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,
    },
  };
}

Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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],
      };
    })
  );
Enter fullscreen mode Exit fullscreen mode

posts variable will look somethings like this -

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

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,
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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,
  };
}
Enter fullscreen mode Exit fullscreen mode

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,
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (5)

Collapse
 
funnypan profile image
panfan

my site build by next.js and mdx:
github.com/Manonicu/site
manon.icu

Collapse
 
sanketss84 profile image
Sanket Sonvane

simple clean and minimal design love it

Collapse
 
undefinedzack profile image
undefinedzack • Edited

Hey I was trying to use tailwind css with next-mdx-remote but there was an issue which I'm not able to solve. Can you help me out?

stackoverflow.com/questions/705395...

Collapse
 
didslm profile image
diar.dev
Collapse
 
said_mounaim profile image
Said Mounaim

Thanks