Next.js: A Guide to Building a Blog with Markdown and MDX

In the ever-evolving landscape of web development, creating a blog that’s both visually appealing and content-rich can be a significant challenge. Developers often grapple with the complexities of content management systems (CMS), the limitations of static site generators, and the desire for a seamless writing and publishing workflow. This tutorial will delve into building a blog using Next.js, a powerful React framework, and the versatility of Markdown and MDX (Markdown Extended) for content creation. We’ll explore how to handle content, display it, and create a dynamic, SEO-friendly blog that’s easy to maintain and scale.

Why Choose Next.js, Markdown, and MDX?

Next.js offers a compelling blend of features that make it ideal for building blogs. Its server-side rendering (SSR) and static site generation (SSG) capabilities provide excellent performance and SEO benefits. Markdown allows writers to focus on content without getting bogged down in HTML, and MDX extends Markdown to include React components, making it a perfect fit for interactive and dynamic content. This combination allows for a clean, efficient, and user-friendly blogging experience.

Benefits of Next.js

  • Performance: Next.js’s built-in optimization features and ability to generate static sites lead to fast loading times.
  • SEO: SSR and SSG are great for search engine optimization, making your blog discoverable.
  • Developer Experience: Next.js provides a streamlined development experience with features like hot module replacement (HMR) and built-in routing.
  • Flexibility: Next.js can be used to build a variety of web applications, including blogs, e-commerce sites, and more.

Benefits of Markdown and MDX

  • Simplicity: Markdown is easy to learn and use, allowing writers to focus on content.
  • Readability: Markdown’s plain text format is easy to read and edit.
  • Extensibility: MDX allows you to embed React components within your Markdown content, adding interactivity and dynamic elements.
  • Portability: Markdown files are easily portable and can be used in various platforms.

Setting Up Your Next.js Project

Let’s start by setting up a new Next.js project. Open your terminal and run the following command:

npx create-next-app my-blog --typescript

This command will create a new Next.js project named “my-blog” using TypeScript. Navigate into your project directory:

cd my-blog

Now, install the necessary dependencies for Markdown and MDX support. We’ll use `gray-matter` to parse frontmatter (metadata) from our Markdown files, `remark` and `remark-html` to transform Markdown to HTML, and `next-mdx-remote` for rendering MDX with Next.js.

npm install gray-matter remark remark-html next-mdx-remote @mdx-js/react

Creating the Blog Post Structure

We’ll organize our blog posts in a directory called `posts` at the root of our project. Inside the `posts` directory, each blog post will be a separate Markdown or MDX file. Each file will include frontmatter at the top, which is metadata such as the title, date, and author of the post.

Create a `posts` directory in your project’s root and then create a sample blog post file, such as `posts/first-post.mdx`. Here’s an example:


--- 
title: "My First Blog Post"
date: "2024-01-26"
author: "John Doe"
--- 

<p>Welcome to my first blog post! This is where I'll share my thoughts and experiences.</p>



<p>More content here...</p>

In this example, the frontmatter provides the title, date, and author. The rest of the file contains the blog post content. Notice the “ which demonstrates how MDX allows to embed React components.

Fetching and Processing Blog Posts

Next, we need to create functions to read and process these Markdown/MDX files. We’ll create a utility file, perhaps named `lib/posts.ts`, to handle these tasks.

Here’s the code for `lib/posts.ts`:


import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { serialize } from 'next-mdx-remote/serialize';
import rehypePrism from '@mapbox/rehype-prism';

const postsDirectory = path.join(process.cwd(), 'posts');

export interface PostData {
    slug: string;
    title: string;
    date: string;
    author: string;
    content: string;
}

export async function getSortedPostsData(): Promise {
  const fileNames = fs.readdirSync(postsDirectory);

  const allPostsData = await Promise.all(
    fileNames.map(async (fileName) => {
      const slug = fileName.replace(/.mdx?$/, '');
      const fullPath = path.join(postsDirectory, fileName);
      const fileContents = fs.readFileSync(fullPath, 'utf8');

      const { data, content } = matter(fileContents);

      const mdxSource = await serialize(content, {
          mdxOptions: {
              rehypePlugins: [rehypePrism],
          },
      });

      return {
        slug,
        ...(data as { title: string; date: string; author: string }),
        content: mdxSource.compiledSource,
      };
    })
  );

  return allPostsData.sort(({ date: a }, { date: b }) => {
    if (a <b> b) {
      return -1;
    } else {
      return 0;
    }
  });
}

export async function getAllPostSlugs() {
    const fileNames = fs.readdirSync(postsDirectory);

    return fileNames.map(fileName => {
        return {
            params: {
                slug: fileName.replace(/.mdx?$/, '')
            }
        }
    })
}

export async function getPostData(slug: string) {
  const fullPath = path.join(postsDirectory, `${slug}.mdx`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');

  const { data, content } = matter(fileContents);

  const mdxSource = await serialize(content, {
      mdxOptions: {
          rehypePlugins: [rehypePrism],
      },
  });

  return {
      slug,
      ...(data as { title: string; date: string; author: string }),
      content: mdxSource.compiledSource,
  };
}

Let’s break down this code:

  • `getSortedPostsData()`: This function reads all files from the `posts` directory, extracts frontmatter, and sorts the posts by date. It also uses `serialize` from `next-mdx-remote` to transform MDX content into a format that can be rendered by Next.js. We also use `rehypePrism` to enable syntax highlighting in our code blocks.
  • `getAllPostSlugs()`: This function retrieves all the slugs (filenames without the extension) of our blog posts, which is needed for creating dynamic routes.
  • `getPostData(slug)`: This function reads a specific post based on its slug, extracts frontmatter and content, and returns the post data.

Creating Blog Post Listing Page

Now, let’s create a page to list all of our blog posts. We’ll create a new file at `pages/index.tsx`.


import { GetStaticProps } from 'next';
import Link from 'next/link';
import { PostData, getSortedPostsData } from '../lib/posts';

interface Props {
    allPostsData: PostData[];
}

export default function Home({ allPostsData }: Props) {
  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {allPostsData.map(({ slug, title, date }) => (
          <li>
            
              <a>{title}</a>
            
            <br />
            <small>{date}</small>
          </li>
        ))}
      </ul>
    </div>
  );
}

export const getStaticProps: GetStaticProps = async () => {
  const allPostsData = await getSortedPostsData();
  return {
    props: {
      allPostsData,
    },
  };
};

In this page:

  • We import `getSortedPostsData()` from `lib/posts.ts` to fetch all blog post data.
  • `getStaticProps` is used to fetch data at build time. This is a Next.js function that allows us to pre-render the page with the blog post data.
  • We map through the `allPostsData` array to display a list of blog posts, each with a link to its individual page.

Creating Dynamic Post Pages

Next, we need to create dynamic routes for our individual blog posts. This is done using the `[slug].tsx` file, which will be located in a `pages/posts` directory.

Create a directory `pages/posts` in your project’s root and then create a file `pages/posts/[slug].tsx`.


import { GetStaticPaths, GetStaticProps } from 'next';
import { MDXRemote, MDXRemoteProps } from 'next-mdx-remote';
import { getAllPostSlugs, getPostData } from '../../lib/posts';

interface Props {
  postData: {
    title: string;
    date: string;
    author: string;
    content: string;
    slug: string;
  };
}

export default function Post({ postData }: Props) {
  return (
    <div>
      <h1>{postData.title}</h1>
      <p>Published: {postData.date} by {postData.author}</p>
      
    </div>
  );
}

export const getStaticPaths: GetStaticPaths = async () => {
  const paths = await getAllPostSlugs();
  return {
    paths,
    fallback: false,
  };
};

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const postData = await getPostData(params?.slug as string);
  return {
    props: {
      postData,
    },
  };
};

Here’s what this code does:

  • `getStaticPaths()`: This function tells Next.js which paths to pre-render. It uses `getAllPostSlugs()` from `lib/posts.ts` to get all available slugs.
  • `getStaticProps()`: This function fetches the data for a specific post based on the slug. It uses `getPostData()` from `lib/posts.ts` to retrieve the post data.
  • The component renders the post’s title, date, author, and content. The `MDXRemote` component is used to render the MDX content.

Styling Your Blog

You can use CSS modules, styled-components, or any other styling solution to style your blog. For simplicity, let’s add some basic styles to the `pages/index.tsx` and `pages/posts/[slug].tsx` files.

In `pages/index.tsx` add the following styles:


import styles from '../styles/Home.module.css';

export default function Home({ allPostsData }: Props) {
  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {allPostsData.map(({ slug, title, date }) => (
          <li>
            
              <a>{title}</a>
            
            <br />
            <small>{date}</small>
          </li>
        ))}
      </ul>
    </div>
  );
}

Create a `styles/Home.module.css` file in your project’s root and add the following styles:


.container {
  padding: 2rem;
}

.postItem {
  margin-bottom: 1rem;
  padding: 1rem;
  border: 1px solid #ccc;
  border-radius: 5px;
}

In `pages/posts/[slug].tsx` add the following styles:


import styles from '../../styles/Post.module.css';

export default function Post({ postData }: Props) {
  return (
    <div>
      <h1>{postData.title}</h1>
      <p>Published: {postData.date} by {postData.author}</p>
      
    </div>
  );
}

Create a `styles/Post.module.css` file in your project’s root and add the following styles:


.container {
  padding: 2rem;
}

These are basic examples, and you can customize them to match your desired design.

Adding a Custom Component (MDX Example)

MDX allows you to include React components within your Markdown content. This is one of the most powerful features of MDX. Let’s create a custom component called `MyCustomComponent` and use it in our blog post.

Create a file named `components/MyCustomComponent.tsx`:


export default function MyCustomComponent() {
    return (
        <div>
            <h2>This is a custom component!</h2>
            <p>You can add any React component here.</p>
        </div>
    );
}

In your MDX file (e.g., `posts/first-post.mdx`), you can then import and use this component:


--- 
title: "My First Blog Post"
date: "2024-01-26"
author: "John Doe"
--- 

<p>Welcome to my first blog post! This is where I'll share my thoughts and experiences.</p>



<p>More content here...</p>

Handling Images

To include images in your blog posts, you can simply add them to your Markdown/MDX files using the standard Markdown syntax:


![Alt text](path/to/image.jpg)

You can store your images in a directory like `public/images`. Ensure your image paths are relative to the `public` directory.

For image optimization, Next.js provides the `next/image` component. This component automatically optimizes images, serving different sizes and formats based on the user’s device. To use it, install it and import it into your component:

npm install next

Then import it into your component:


import Image from 'next/image'

export default function Post({ postData }: Props) {
  return (
    <div>
      <h1>{postData.title}</h1>
      <p>Published: {postData.date} by {postData.author}</p>
      
      
    </div>
  );
}

Common Mistakes and How to Fix Them

1. Incorrect File Paths

Mistake: Using incorrect file paths for images or other assets. This can lead to broken images or missing content.

Fix: Double-check your file paths. Ensure that they are relative to the correct directory and that the file names are spelled correctly. Using the `public` directory for static assets is recommended.

2. Frontmatter Errors

Mistake: Typos or errors in your frontmatter can cause parsing errors and prevent your blog posts from displaying correctly.

Fix: Carefully check your frontmatter for any typos or syntax errors. Make sure you have the correct keys and values. Using a linter or code editor with Markdown support can help catch these errors early.

3. Missing Dependencies

Mistake: Forgetting to install the necessary dependencies for Markdown and MDX processing.

Fix: Make sure you have installed all the required dependencies: `gray-matter`, `remark`, `remark-html`, `next-mdx-remote`, and `@mdx-js/react`. Run `npm install` or `yarn install` to ensure all dependencies are installed.

4. Incorrect MDX Syntax

Mistake: Using incorrect syntax when writing MDX content, especially when including React components.

Fix: Make sure you are using the correct MDX syntax. React components should be imported and used within your MDX files. Double-check your component imports and usage.

5. Caching Issues

Mistake: Not clearing the cache when making changes to your blog posts or configurations.

Fix: When you make changes to your blog posts or configurations, restart your development server or rebuild your site to ensure the changes are reflected. You may also need to clear your browser’s cache.

SEO Best Practices

To ensure your blog ranks well in search engines, follow these SEO best practices:

  • Keyword Research: Identify relevant keywords for your blog posts.
  • Title Tags: Use descriptive title tags that include your target keywords (keep them under 60 characters).
  • Meta Descriptions: Write compelling meta descriptions (under 160 characters) that encourage clicks.
  • Header Tags: Use header tags (H1-H6) to structure your content and highlight important keywords.
  • Image Optimization: Optimize your images by using descriptive alt text and compressing them for faster loading times.
  • Internal Linking: Link to other relevant posts on your blog to improve user engagement and SEO.
  • External Linking: Link to authoritative sources to provide credibility and enhance your content.
  • Mobile-Friendliness: Ensure your blog is responsive and mobile-friendly.
  • Sitemap: Submit a sitemap to search engines to help them crawl and index your content.
  • Content Freshness: Regularly update your blog with new and relevant content.

Summary/Key Takeaways

Building a blog with Next.js, Markdown, and MDX offers a powerful and efficient way to create and manage content. By leveraging the performance of Next.js, the simplicity of Markdown, and the flexibility of MDX, developers can create dynamic, SEO-friendly blogs that are easy to maintain and scale. This tutorial provides a step-by-step guide to setting up your blog, fetching and processing blog posts, creating dynamic routes, and adding custom components. By following these guidelines and best practices, you can create a successful blog that attracts readers and ranks well in search engines.

FAQ

1. How do I deploy my Next.js blog?

You can deploy your Next.js blog to various platforms, including Vercel (recommended), Netlify, and other hosting providers. Vercel is especially well-suited for Next.js and provides a seamless deployment experience.

2. Can I use a CMS with Next.js?

Yes, you can integrate a CMS (like Contentful, Strapi, or WordPress) with your Next.js blog. You’ll typically use the CMS’s API to fetch content and display it on your blog.

3. How can I add a comment section to my blog?

You can integrate a comment section using third-party services like Disqus or Commento. These services provide a simple way to add comments to your blog posts.

4. How do I handle pagination in my blog?

You can implement pagination in your blog by fetching a limited number of posts at a time and providing links to navigate between pages. You’ll need to modify your `getStaticProps` function to handle pagination parameters.

5. Can I use TypeScript with this setup?

Yes, this tutorial uses TypeScript. Next.js has excellent TypeScript support, and you can easily type your components and data fetching functions.

The journey of building a blog with Next.js, Markdown, and MDX is a rewarding one. As you delve deeper into the capabilities of these technologies, you’ll discover even more ways to enhance your blog’s functionality and user experience. Remember to experiment, iterate, and continuously learn to create a blog that not only showcases your content but also reflects your personal style. Embrace the flexibility that MDX offers to create dynamic and engaging content, and always keep the user experience at the forefront of your design choices. With each post, with each new feature you integrate, your blog will evolve, becoming a testament to your creativity and expertise. The possibilities are vast, and the only limit is your imagination.