Build a blog by yourself 02 - Next.js & MDX

What are Next.js & MDX

Next.js

Next.js is a open-sourced and React-based framework. We can create SSR and SGR app easiily, and it also support ISR with SWR library. There are completed documents and many examples for us to learn Next.js, one of cases is using MDX/MD on Next.js.

MDX

MDX allow we use JSX in the markdown content. There are plugins providing a good integration on it, we can use custom components or override the markdown sytnx with our components. It's a straightforward way for us writing blog and with less burdensome on maintenance.

How to start?

Next.js provides a CLI tool enabled us to quickliy start building a new Next.js application, we don't need to set up everything by ourselves. I'll use npm in my case showing.

npx create-next-app@latest

If we would like to use TypeScript, the CLI tool also provides --ts or --typescript flag.

npx create-next-app@latest --ts

After entered this command line, we need to enter y and our project name.

set up in next.js app

Then, it will install dependency packages. After installation completed, there are a successful information and some scripts tips to start running project.

next.js install completed

Enter the command line, npm run dev, and open link http://localhost:3000 to see the Next.js app running on the browser.

Run the Next.js app

A .next directory was generated with npm run dev, there are cache, server, and static files of our app.

Pages and routing

In Next.js, all the site pages rendered by the files under pages directory, there are multiple page name rules to map the router.
We need pages for our app, one of is to list all posts and the other one is the content of every post. We create a folder named blog, and then create blog/index.tsx to list posts and blog/[slug].tsx for post content. The directory as shown below, followed Next.js router rule, the default file under blog is blog/index.tsx. When we run on local, the address in blog page is http:localhost:3000/blog. The blog/[slug].tsx is a way to map dynamic path name, the address will be http:localhost:3000/blog/[dynamic-path-name], [dynamic-path-name] setted by Next.js method called getStaticPaths.


The directory of pages

And create _posts folder to place all post wrote with MDX, and there are two mdx file as posts sample.


The directory of pages

How to write MDX

Here's the content what MDX looked like, we can use markdown with html tag, and jsx.

---
title: "Build a blog by yourself 01 - introduction"
date: 'January 18, 2022'
description: 'I would like to be a the enthusiasm ...'
thumbnailUrl: '/assets/blog/how-to-build-this-site/how-to-build-this-site.jpeg'
tags: ['blog', 'javascript', 'Next.js']
---

## The motivation

I would like to be a the enthusiasm and professional engineer.
Keep learning and willing to share to improve myself, 
so I start to write some blogs about what I learned or something interesting.

...

#### Blog and Posts
<img src="/assets/blog/how-to-build-this-site/wireframe-blog.jpg"  alt="Wireframe home" width="100%"/>
<br/>

...

### Use these to improve development
* [eslint](https://eslint.org/)
* [prettier](https://prettier.io/)
* [typescript](https://www.typescriptlang.org/)
* [husky](https://typicode.github.io/husky/#/)

<PostThumbnail
  slug="demo"
  thumbnailUrl="demo.png"
  title="demo title"
  date="'January 18, 2022'"
  description="demo description"
/>

...

On the top of content enclosed with ---, it's a post metadata used YAML sytnx. The sytnx use key and value mapping to write any information on it. We also could set our custom component, the <PostThumbnail> is the example as shown below to link other post:

Build a blog by yourself 01 - introduction

I would like to be a the enthusiasm and professional engineer. Keep learning and willing to share to

Building blog page

There're two matters we need to do, one is list all posts in blog/index.tsx, the other one is showing MDX content in blog/[slug].tsx.

Next.js page and fetch data

In Next.js page

In Next.js, we can use a getStaticProp method to fetch data which running at build time, and return some data, like props data used in page. As follows shown, the basic code looked like this:

const Blog: NextPage<Props>= (props: Props) => {{
  return (
    <div></div>
  )
}
export const getStaticProps = async () => {
  return {
    props: {
      // props data
    }
  }
}
export default Blog

But if we need post list in pagination by url query parameter, such as http://localhost:3000/blog?page=1. We could not get the url query in getStaticProp, because it's only invoked on a build time, and not refresh data when queries are changed.

Next.js provide other method is GetServerSideProps, it's primarily used to fetch data for every time when user with different request, like query paramters are changed. This method is used like getStaticProp, but we can know the query from user request.

export const getServerSideProps: GetServerSideProps = async ({ query }) => {
  // We can use query for data fetching
  return {
    props: {
      // props data
    },
  };
};

Fetch data from _posts folder

To fetch data, we need to read all MDX files from _post directory. I prefer to use an API separately to fetch data, next create lib folder then add a new file named lib/api.tsx.

Before fetching data, we need install the gray-matter to get metadata from MDX file.

npm install -D gray-matter

In api.tsx, the first thing is to import fs, path, and gray-matter.

// api.tsx
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

write a method to get metadata from all posts afterwards. Use method, fs.readdirSync, to get files name array in _posts folder, map file name as path and get metatdata by gray-matter.

// api.tsx
...
const postsDirectory = path.join('_posts');

const readFile = (fileName: string) => {
  const postPath = join(postsDirectory, fileName);
  return fs.readFileSync(postPath, 'utf-8');
};

export const getAllPosts = () => {
  const files = fs.readdirSync(postsDirectory);
  const posts = files.map((fileName) => {
    const markdownWithMeta = readFile(fileName);
    const { data: metadata } = matter(markdownWithMeta);
    return {
      metadata,
      slug: fileName.split('.')[0],
    };
  });
  return posts;
};

After that, we can use getAllPosts api to fetch data in blog/index.tsx. In blog/index.tsx, we use posts as our props to render them on page. Let's add thumbnail, title and description in every post block, and also use <Link> component from 'next/link' to link post by click event.

// index.tsx
import { getAllPosts } from '../../lib/api';
import Link from 'next/link'

const Blog: NextPage<Props>= (props: Props) => {{
  return (
    <>
      {
        posts.map((post, index) => <div>
          <img url={post.metadata.thumbnailUrl}/>
          <h2>{post.metadata.title}</h2>
          <p>{post.metadata.description}</p>
          <Link href={`/blog/${post.slug}`}>
            <a>Read more...</a>
          </Link>
        </div> )
      }
    </>
  )
}
export const getStaticProps = async () => {
  const posts = getAllPosts();
  return {
    props: {
      posts
    }
  }
}

In slug.tsx, it will generate all path list at build time use by getStaticPaths.

// [slug].tsx
export const getStaticPaths = () => {
  const posts = getAllPosts();
  const paths = posts.map((post) => {
    return {
      params: {
        slug: post.slug,
      },
    };
  });
  return {
    paths,
    fallback: false,
  };
};

Set MDX in [slug].tsx page

In the beginning, we add a new method named getPostBySlug in lib/api.tsx, and call it when [slug].tsx fetch MDX files with getStaticProps

// lib/api.tsx
...
export const getPostBySlug = (slug: string) => {
  const fileContents = readFile(`${slug}.mdx`);
  const { data: metadata, content } = matter(fileContents);
  return { metadata, content, slug };
};
...

Before calling getPostBySlug, we need to install next-mdx-remote which support to add MDX in Next.js app. There is a method called serialize in next-mdx-remote, it parse and compile the MDX string thus rendered on page.

npm install -D next-mdx-remote

The next step is called getPostBySlug which fetch post content and metadata in getStaticProps, and use them as props of blog/[slug].txt page.

// [slug].tsx
import { getPostBySlug } from '../../lib/api';
import matter from 'gray-matter'

export const getStaticProps = async ({ params: { slug } }: Params) => {
  const { metadata, content } = getPostBySlug(slug);
  const { mdxSource } = await serialize(content);

  return {
    props: {
      metadata,
      mdxSource,
    },
  };
};

After that, we import MDXRemote from next-mdx-remote, and use the <MDXRemote/> to render MDX content. Not only the MDX source, but we could set our custom components in the components prop. I use <PostThumbnail> as following case.

// [slug].tsx
...
import { PostThumbnail } from '../../components';

const markdownComponents = {PostThumbnail}
const Post = ({ mdxSource, prevPost, nextPost }: Props) => (
  <MDXRemote {...mdxSource} components={markdownComponents}/>
)
...
export default Post;

There is a basic content shown in page. Because we also get metadata from getPostBySlug, we could use thumbnailUrl as our post banner or showing any information. Now, we have a basic blog with features of listing all posts and clicking to link any post content.

Next time, I'll implement how to use tailwind CSS beautifying our blog as shown below in the figure, tailwind is a CSS library based on atom class. It's a new way to write style but systematic and with good performance.

Use tailwind CSS
See you next time ! :-)

References