Creating Dynamic Blogs Using MDX Remote in Next.js

In the world of web development, Markdown has become a popular choice for writing content due to its simplicity and readability.

Next.js, a powerful React framework, provides an efficient way to transform these Markdown files into fully functional blog posts using the next-mdx-remote/rsc package.

This blog will guide you through the process of setting up a Next.js 14 application to convert Markdown files into dynamic blogs.

Setting Up Your Next.js Project

First, ensure you have Node.js installed on your system. Then, create a new Next.js project by running:

npx create-next-app@latest markdown-blog
cd markdown-blog

Installing Dependencies

To handle Markdown files, you’ll need to install next-mdx-remote/rsc along with @types/mdx for handling types:

npm install next-mdx-remote@rsc

Creating the Markdown Files

Store your Markdown files in a directory within your project, typically named content. Each file represents a blog post. For example:

/content
 - my-first-post.mdx
 - my-second-post.mdx

Setting Up the MDX Compiler

Now, we have to create a utility file to handle the loading and compiling of these Markdown files.

Let’s talk about the methods to be used to convert the Markdown to HTML.

🔹getPost Method

The getPost method fetches and compiles a single blog post specified by its slug. It begins by calling the loadPost function, which constructs the file path using the provided slug and reads the content of the corresponding Markdown file from the filesystem. This content, which includes both the raw Markdown and any frontmatter metadata, is then passed to the compileMDX function from the next-mdx-remote/rsc package.

The compileMDX function is crucial as it converts the Markdown into MDX (Markdown for the component era), allowing React components to be embedded within the Markdown content. The method specifies components (like custom headers) and options (such as parsing frontmatter) to be used during compilation. For example, you can add your custom H1 component using the components props like this:

components: {
   h1: (props) => <H1 {...props} />
},

The result is a fully parsed and ready-to-render MDX content object, which includes both the React components and any metadata defined in the front matter.

🔹getPosts Method

The getPosts method retrieves multiple blog posts based on specified criteria such as tags, sorting order, and pagination. It first reads all the filenames from the content directory, then uses these filenames to fetch and compile each post asynchronously using the getPost method. This results in an array of posts, each containing structured data including frontmatter and a slug.

Post-filtering is applied if tags are specified, filtering out posts that do not contain the specified tags. Sorting is also handled, allowing posts to be ordered by date either from newest to oldest or vice versa. Finally, pagination is implemented by slicing the array of posts according to the specified page number and limit, ensuring that only a subset of posts is returned at a time.

Together, these methods provide a robust backend mechanism for handling blog content in a Next.js application, leveraging the power of MDX to seamlessly integrate dynamic React components within static Markdown content.

Here’s a simplified version of what this might look like:

// lib/posts.js

import fs from 'fs'
import path from 'path'
import { compileMDX } from 'next-mdx-remote/rsc'
import H1 from '@/components/h1'

export function loadPost(slug) {
  const filename = slug.endsWith('.mdx') ? slug : `${slug}.mdx`

  return fs.readFileSync(
    path.join(process.cwd(), 'content', filename)
  )
}

export async function getPost(slug) {
  const source = loadPost(slug)

  return await compileMDX({
    source,
    components: {
      h1: (props) => <H1 {...props} />
    },
    options: {
      parseFrontmatter: true
    }
  })
}


export async function getPosts({
   newest = true, page = 1, limit = 4, tags
} = {}) {
   const files = fs.readdirSync(
     path.join(
       process.cwd(), 'content'
     )
   )

const posts = await Promise.all(
  files.map(async filename => {
    const { frontmatter } = await getPost(filename)

    return {
      frontmatter,
      slug: filename.replace('.mdx', '')
    }
  })
)

let filteredPosts = posts

if (tags) {
  filteredPosts = filteredPosts.filter(
    post => post.frontmatter.tags.some(
       tag => tags.includes(tag)
    )
  )
}

if (newest) {
  // by the newest
     filteredPosts.sort(
     (a, b) => {
     const dateA = new Date(a.frontmatter.date)
     const dateB = new Date(b.frontmatter.date)
        return dateB - dateA
     }
   )
     } else {
filteredPosts.sort(
    (a, b) => {
       const dateA = new Date(a.frontmatter.date)
       const dateB = new Date(b.frontmatter.date)
         return dateA - dateB
      }
   )
}

const startIndex = (page - 1) * limit // 0
const endIndex = page * limit // 10

return {
  posts: filteredPosts.slice(startIndex, endIndex),
  pageCount: Math.ceil(filteredPosts.length / limit)
 }
}

Stay Updated for More Insights on Web Development and Next.js.

Fetching and Displaying Posts

Now it’s time to check if the methods are cooked properly. We need to set up a dynamic route to handle individual blog posts. Create a file under pages/posts/[slug].js:

import { notFound } from 'next/navigation'
import { getPost as getPostNotCached, getPosts } from '@/lib/posts'
import { cache } from 'react'

const getPost = cache(
  async (slug) => await getPostNotCached(slug)
)

export async function generateStaticParams() {
  const {posts} = await getPosts({ limit: 1000 })
  return posts.map(post => ({
   slug: post.slug
  }))
}

Here, the generateStaticParams function fetches a list of blog posts and generates static parameters for each post. It uses the getPosts function to retrieve up to 1000 posts. The function then maps over the array of posts, extracting the slug of each post to create an array of objects. Each object contains a slug property, which is used as a parameter for static site generation in Next.js applications. This is particularly useful for generating static paths in getStaticPaths for dynamic routes.

export async function generateMetadata({ params }) {
  try {
     const { frontmatter } = await getPost(params.slug)
     return frontmatter
  }  catch (e) {
     // handle error
  }
}

The generateMetadata function fetches metadata for a blog post based on a slug. It takes an object containing params as an argument, which includes the slug of the post. The function attempts to retrieve the post using the getPost function and returns the frontmatter of the post, which typically contains metadata such as title, date, tags, etc. If the post cannot be fetched, the function handles the exception silently and does not return any metadata. This function is useful for generating SEO-friendly metadata for each blog post page.

Now it’s time to fetch the posts using the getPost method:

export default async function BlogPage({ params }) {
  const post = await getPost(params.slug)

  return (
    <article className="prose dark:prose-invert">
      …
      {post.content}
    </article>
  )
}
coma

Conclusion

In conclusion, by setting up your Next.js application with next-mdx-remote/rsc, you can easily transform Markdown files into dynamic and interactive blog posts. This approach not only simplifies content management by allowing you to write posts in Markdown but also enhances the functionality of your blog by enabling the use of React components directly within your content. It’s a powerful combination that makes your blog both easy to maintain and rich in features, perfect for developers looking to create a modern, scalable blogging platform.

Keep Reading

Keep Reading

  • Service
  • Career
  • Let's create something together!

  • We’re looking for the best. Are you in?