19
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!
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.
- A decent knowledge of Next.JS framework
- Dependencies -
path fs gray-matter next-mdx-remote
- Tailwind CSS
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 |
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;
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,
},
};
}
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;
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>
);
};
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;
}
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
Twitter - shaancodes
Github - shaan-alam
YouTube - shaancodes
Instgram - shaancodes
19