DEV Community

Cover image for How to Create a Blog Using NextJS v14 and MDX: A Comprehensive Guide
PineAppleGrits
PineAppleGrits

Posted on • Originally published at ginos.codes

How to Create a Blog Using NextJS v14 and MDX: A Comprehensive Guide

It is very effective to develop a blog since it helps to showcase and share acquired knowledge, pertinent experiences, and updates to a larger audience. More recently, I integrated a blog functionality on my web site with NextJS v14, MDX, and other libs. This blog post will guide you through the steps that I followed to incorporate the blog featuring updating parts, as well as configuring Next. js and also adding all the needed dependencies.

Why I Chose to Blog

Therefore, experiencing real-life projects, I began this blog to give an open look into the work process and describe my thinking and actions when turning concepts into tangible products as well as my reasons for those actions. Being a developer myself, most of the time I dwell on projects where an application begins as a mere idea in the developer’s mind and in concrete reality in a fairly short time. Documenting this journey serves several purposes:

  1. 🕵️‍♂️ Transparency: To elaborate, the following paragraphs describe actions I choose to incorporate in ensuring that my projects are efficient and effective. For the aspiring developer on the one hand, oftentimes it is useful to observe how a more experienced coworker or a fellow student, as the case may be, is going about solving the problem.

  2. 💡 Inspiration: Sharing my motivations and the reasons behind my projects can inspire others to pursue their own ideas. Understanding the "why" behind a project often provides the fuel needed to push through challenges and stay committed.

  3. 📚 Documentation: Keeping a record of my development journey helps me track my progress and reflect on the lessons learned. It's a valuable resource for future reference and continuous improvement.

  4. 🗣️ Community Engagement: A blog makes it easy to have two-way communication, getting feedback from the readers. There is also interaction by making comments of different stories and articles to get new ideas, collaboration and fellowship from other readers.

My objective is to note down and share all the stages of idea formation and turning it into an actual project and thus motivate people sharing their ideas. It is my way of putting something back into the developer population and helping to build the knowledge base.

So... How i did it?

Adding and Configuring the dependencies

Next.js supports MDX (Markdown for JSX) out of the box with some additional configuration. Here’s how I set it up:

Install Required Packages

I installed the necessary MDX packages and related dependencies:

npm install @mdx-js/loader @mdx-js/react @next/mdx @tailwindcss/typography 
Enter fullscreen mode Exit fullscreen mode

Update next.config.js

We must add the withMDX plugin to the Next.js configuration file.

// next.config.js
const withMDX = require("@next/mdx")();

const nextConfig = {
  // Rest of the configuration
  pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"],
};

module.exports = withMDX(nextConfig);
Enter fullscreen mode Exit fullscreen mode

Update tailwindcss.config.ts

I'm using tailwind so i must add the typography plugin to the tailwind config file.

// tailwindcss.config.ts
const config = {
  // Rest of the configuration
  plugins: [require("@tailwindcss/typography")],
}
Enter fullscreen mode Exit fullscreen mode

Create required components to render the MDX

We will create a mdxComponents file that will allow us to define how each element of the MDX will be rendered.

// components/MdxComponents.tsxx
import Link from "next/link";
import { MDXComponents } from "mdx/types";
import { Ref, RefAttributes } from "react";
import { Code } from "bright";
export const mdxComponents: MDXComponents = {
  pre: (props) => (
    <Code
      theme={"github-dark"}
      {...props}
      style={{
        margin: 0,
      }}
    />
  ),
  a: ({ children, ref, ...props }) => {
    return (
      <Link href={props.href || "."} ref={ref as Ref<HTMLAnchorElement> | undefined} {...props}>
        {children}
      </Link>
    );
  },
  File: ({ children, path, ...props }) => {
    return (
      <div className="bg-gray-950 pt-1 rounded-3xl">
        <div className="flex items-center ml-4 my-2 italic font-semibold">{path}</div>
        {children}
      </div>
    );
  },
};

export default mdxComponents;
Enter fullscreen mode Exit fullscreen mode

For example in the code above, we are defining how the pre tag will be rendered, we are using the Code component from the bright library to render the code blocks with syntax highlighting. We are also defining how the a tag will be rendered, we are using the Link component from Next.js to render the links. We are also defining a custom File component that will render a file path above the code block.

The File component is exactly what im using to render the code blocks in this blog post!

Let's create the component to render the MDX!

// components/BlogCard.tsxx
import { MDXRemote } from "next-mdx-remote/rsc";
import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import remarkA11yEmoji from "@fec/remark-a11y-emoji";
import remarkToc from "remark-toc";
import mdxComponents from "./MdxComponents";

export default function BlogPost({ children }: { children: string }) {
  return (
    <div className="prose prose-invert min-w-full">
      <MDXRemote
        source={children}
        options={{
          mdxOptions: {
            remarkPlugins: [
              // Adds support for GitHub Flavored Markdown
              remarkGfm,
              // Makes emoji accessible ! adding aria-label
              remarkA11yEmoji,
              // generates a table of contents based on headings
              [remarkToc, { tight: true }],
            ],
            // These work together to add IDs and linkify headings
            rehypePlugins: [rehypeSlug, rehypeAutolinkHeadings],
          },
        }}
        components={mdxComponents}
      />
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

This is the component that will render the blog post, it uses the MDXRemote component from next-mdx-remote/rsc to render the MDX content. We also pass some options to the MDXRemote component to enable support for GitHub Flavored Markdown, accessible emojis, and table of contents generation. The mdxComponents object contains the components that will be used to render the MDX content.

Create the logic to fetch the blog posts

We will define the types for the blog posts and create a function to fetch all the posts.

// types.d.ts
export type IBlogPost = {
  date: string;
  title: string;
  description: string;
  slug: string;
  tags: string[];
  body: string;
};
Enter fullscreen mode Exit fullscreen mode

We will store our .mdx files under the /posts path, and we will use the gray-matter library to parse the front matter of each file.
Create a file to define the functions to fetch all blog posts.

// lib/blog.ts  
import matter from "gray-matter";
import path from "path";
import fs from "fs/promises";
import { cache } from "react";
import { IBlogPost } from "@/types";
export const getPosts: () => Promise<IBlogPost[]> = cache(async () => {
  const posts = await fs.readdir("./posts/");

  return Promise.all(
    posts
      .filter((file) => path.extname(file) === ".mdx")
      .map(async (file) => {
        const filePath = `./posts/${file}`;
        const postContent = await fs.readFile(filePath, "utf8");
        const { data, content } = matter(postContent);

        // Not published? No problem, don't show it.
        if (data.published === false) {
          return undefined;
        }

        return { ...data, body: content } as IBlogPost;
      })
      .filter((e) => e)
  ) as Promise<IBlogPost[]>;
});

export async function getPost(slug: string) {
  const posts = await getPosts();
  return posts.find((post) => post?.slug === slug);
}

export default getPosts;

Enter fullscreen mode Exit fullscreen mode

The getPosts function reads all the files in the /posts directory, filters out the non-MDX files, and parses the front matter using gray-matter. It then returns an array of objects containing the post data and body content. The getPost function fetches a specific post based on its slug.

Create a Blog Page

Now let's create the page where we wll display all the blog posts,
I created a group to be able to define a layout for the blog pages, you dont need to if you dont need it.

// app/(blog)/blog/page.tsxx
// Here's where we will display all the blog posts
import BlogCard from "@/components/BlogCard";
import getPosts from "@/lib/blog";

export default async function PostPage() {
  const posts = await getPosts();

  return (
    <>
      <h1 className="text-5xl">
        The <span className="font-bold">Blog</span>.
      </h1>
      <div className="flex flex-col space-y-10 mt-4 p-5">
        <div>
          <ol className="group/list">
            {posts.map((post, i) => (
              <li
                className={`mb-12 animate-ease-in animate-delay-500 ${i % 2 == 0 ? "animate-fade-right" : "animate-fade-left"}`}
                key={i}
              >
              {/* Remember to always define a key when doing a map function.*/}
                <BlogCard {...post} key={i} />
              </li>
            ))}
          </ol>
        </div>
      </div>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

The PostPage component fetches all the blog posts using the getPosts function and maps over them to render a BlogCard component for each post. The BlogCard component will display the post title, description, and tags.

And then we can create the page for the specific post.

// app/(blog)/[slug]/page.tsxx
import BlogPost from "@/components/BlogPost";
import getPosts, { getPost } from "@/lib/blog";

import { notFound } from "next/navigation";
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((post) => ({ slug: post?.slug }));
}
export default async function PostPage({
  params,
}: {
  params: {
    slug: string;
  };
}) {
  const post = await getPost(params.slug);
  if (!post) return notFound();
  return <BlogPost>{post?.body}</BlogPost>;
}

Enter fullscreen mode Exit fullscreen mode

The PostPage component fetches the specific post based on the slug provided in the URL. If the post is not found, it returns a 404 page using the notFound function from next/navigation. The BlogPost component renders the post content using the MDXRemote component.

Create a Sitemap

Creating a sitemap is essential for search engine optimization (SEO) and helps search engines discover and index your website's pages. We can create a sitemap using a serverless function in Next.js.

// app/sitemap.ts
import { getPosts } from "@/lib/blog";

export default async function sitemap() {
  // Define the routes that should be included in the sitemap
  const routes = ["", "/blog"].map((route) => ({
    url: `https://ginos.codes${route}`,
    lastModified: new Date().toISOString().split("T")[0],
  }));

  // Get all posts and create a sitemap route for each one
  const posts = await getPosts();
  const blogs = posts.map((post) => ({
    url: `https://ginos.codes/blog/${post.slug}`,
    lastModified: new Date(post.date).toISOString().split("T")[0],
  }));

  return [...routes, ...blogs];
}

Enter fullscreen mode Exit fullscreen mode

The sitemap function defines the routes that should be included in the sitemap. It includes the homepage and the blog page. It then fetches all the blog posts and creates a sitemap route for each post, including the post URL and last modified date.

Conclusion

In this guide, we covered the complete process of setting up a blog using NextJS v14 and MDX. From installing and configuring necessary dependencies to creating and displaying blog posts, each step is crucial for a functional and aesthetically pleasing blog. By following these steps, you can easily set up a blog that is not only easy to manage but also SEO-friendly.

Top comments (0)