• Skip to primary navigation
  • Skip to main content
  • Skip to footer

Codemotion Magazine

We code the future. Together

  • Discover
    • Events
    • Community
    • Partners
    • Become a partner
    • Hackathons
  • Magazine
    • Backend
    • Frontend
    • AI/ML
    • DevOps
    • Dev Life
    • Soft Skills
    • Infographics
  • Talent
    • Discover Talent
    • Jobs
    • Manifesto
  • Companies
  • For Business
    • EN
    • IT
    • ES
  • Sign in

Antonello ZaniniApril 5, 2023

How to Create an MDX Blog in TypeScript With Next.js

Frontend
blog, next.js, mdx blog
facebooktwitterlinkedinreddit

MDX is a powerful combination of Markdown and React components that allows you to create dynamic and interactive content. This makes it the perfect markup language for creating a blog. Next.js natively supports MDX, so let’s use both and create an MDX Next.js TypeScript blog!

At the end of this tutorial, you will know how to build the following MDX-based blog in Next.js and TypeScript. Here you can check a live demo of how the app would look like.

Recommended article
May 6, 2025

Top 10 Free Web Hosting Services Every Developer Should Know

Lucilla Tomassi

Lucilla Tomassi

Frontend

Let’s dive into MDX in Next.js!

What Is MDX?

MDX, short for Markdown eXtended, is a markup language that supports the use of JSX within Markdown documents. In other words, MDX combines the simplicity of Markdown with the power of React components. This enables you to create interactive content with no effort.

This is what a sample of MDX looks like:

import SomeComponent from './SomeComponent';

# My MDX Blog Post

Here's some text for my blog post. I can include React components like this:

<SomeComponent prop1="value1" prop2="value2" />

I can also include regular Markdown:

## Section Heading

- List Item 1
- List Item 2
- List Item 3Code language: HTML, XML (xml)

Typically, MDX is used for content that requires both rich formatting and interactivity, such as documentation or blog posts. By allowing React components to be embedded directly into a Markdown document, MDX simplifies the creation of dynamic content that would be difficult to achieve with simple Markdown.

MDX is supported by several libraries and frameworks, including Next.js, Gatsby, Nuxt, and other static site generators. It is also endorsed by popular documentation sites like Storybook and Docz.

Markdown/MDX in Next.js

Next.js is a popular framework for building server-side web applications with React. Specifically, it comes with built-in support for Markdown and MDX through several tools, including next-mdx-remote. This package developed by the community allows Markdown or MDX content to be fetched directly inside getStaticProps() or getStaticPaths() with no extra configuration required.

With next-mdx-remote, you can load MDX content from a variety of sources, including local files, remote URLs, or a database. The package also comes with a powerful MDX render. This is able to transform MDX content into React components and can be configured with custom UI components.

Take a look at the official documentation to find out more about using Markdown and MDX. Now, it is time to learn how to build a blog based on MDX files in TypeScript with Next.js!

Building an MDX Blog in Next.js and TypeScript

Before getting started, make sure you have npm 18+ installed on your machine. Otherwise, download it here.

Follow this step-by-step tutorial and learn how to build an MDX-based blog with TS in Next.js!

Set up a Next.js TypeScript project
Next.js officially supports TypeScript. Launch the command below in the terminal to create a new Next.js TypeScript project with:

npx create-next-app@latest --tsCode language: CSS (css)

You will be asked some questions. Answer as follows:

√ What is your project named? ... nextjs-markdown-blog
√ Would you like to use ESLint with this project? ... Yes
√ Would you like to use `src/` directory with this project? ... No
√ Would you like to use experimental `app/` directory with this project? ... No
√ What import alias would you like configured? ... @/*Code language: JavaScript (javascript)

This will initialize a Next.js TS project inside the nextjs-markdown-blog directory. Enter the folder in the terminal and launch the Next.js demo app with:

cd nextjs-markdown-blog
npm run dev 

If everything went as expected, you should be seeing the default Create Next App view below:

The Create Next App general view

Great! You now have a Next.js project ready to be turned into an MDX blog. Keep reading and learn how!

Populate your blog with some MDX files
Your blog will consist of articles read from MDX files. Create a _posts folder in your project and populate it with some .mdx articles. If you lack imagination or time, you can use a Lorem Ipsum generator or ask ChatGPT to generate some content for you.

This is what a sample top-programming-languages.mdx blog post file may look like:

---
title: Top 5 Programming Languages to Learn
description: A brief overview of the most in-demand programming languages you should consider learning in 2023 to stay ahead of the curve in the tech industry.
previewImage: https://source.unsplash.com/A-NVHPka9Rk/1920x1280
---

# Top 5 Programming Languages to Learn
<HeroImage src="https://source.unsplash.com/A-NVHPka9Rk/1920x1280" alt={"main image"} />

Programming languages are the backbone of the digital world, powering everything from websites and mobile apps to data analysis and artificial intelligence. With so many programming languages to choose from, it can be tough to know where to start. In this article, we'll discuss the top 5 programming languages to learn in 2023.
{/* omitted for brevity... */}Code language: PHP (php)

Take a look at the special syntax used at the beginning of the file to define a title, description, and previewImage. That is a YAML frontmatter. If you are not familiar with this concept, a frontmatter is a section of metadata enclosed in triple-dashed lines --- that appears at the beginning of a Markdown or MDX document. Typically, it is in YAML format and provides useful information to describe the content stored in the Markdown/MDX document.

Also, note the HeroImage component used inside the .mdx file. That is a custom React component that will be rendered together with the content contained in the MDX file. You will learn how this is possible in the next steps.

Define the MDX components
Each JSX element mentioned in .mdx files must have a respective React component in the Next.js project. Add a components/mdx folder and prepare to write some custom MDX components. For example, this is the HeroImage React file mentioned early:

// components/mdx/HeroImage.tsx

import React from "react"
import Image from "next/image"

export default function HeroImage({ src, alt }: { src: string; alt: string }) {
  return (
    <div className={"mdx-hero-image"}>
      <Image src={src} alt={alt} fill></Image>
    </div>
  )
}Code language: JavaScript (javascript)

As you can note, it wraps the Next.js’s component in a custom div that you can style as you wish. In particular, HeroImage takes care of representing the main image associated with each article.

Keep in mind that you can also define React components to override the default HTML element used by next-mdx-remote to render MDX content. For example, define a special H1 component for Markdown titles as below:

// components/mdx/H1.tsx

import React from "react"

export default function H1({ children }: { children?: React.ReactNode }) {
  return <h1 className="mdx-h1">{children}</h1>
}Code language: JavaScript (javascript)

Similarly, you can create an H2 component for subtitles:

// components/mdx/H2.tsx

import React from "react"

export default function P({ children }: { children?: React.ReactNode }) {
  return <h2 className="mdx-h2">{children}</h2>
}Code language: JavaScript (javascript)

And a custom P component for text paragraphs:

// components/mdx/P.tsx

import React from "react"

export default function P({ children }: { children?: React.ReactNode }) {
  return <p className="mdx-p">{children}</p>
}Code language: JavaScript (javascript)

What these components have in common is that they are characterized by a custom CSS class. This gives you the ability to style these MDX components as you like, for example with the following CSS rules in styles/global.css:

.mdx-h1 {
  font-size: 52px;
  color: #0d1d30;
}

.mdx-h2 {
  font-size: 36px;
  color: #0d1d30;
}

.mdx-p {
  font-size: 20px;
  margin-bottom: 1.5em;
  line-height: 1.6em;
}

.mdx-hero-image {
  position: relative;
  width: 100%;
  height: 600px;

  img {
    border-radius: 10px;
    object-fit: cover;
    object-position: center;
  }
}Code language: PHP (php)

Time to learn how to use these custom MDX components with next-mdx-remote.

Render MDX in Next.js**
To add server-side MDX rendering capabilities to Next.js, you need to add next-mdx-remote to your project’s dependencies with:

npm install next-mdx-remote

You are now ready to create the Next.js dynamic-content page for your blog posts. In detail, you will use Next.js’s dynamic routes feature. This allows you to populate a template page with the MDX content stored in each.mdx file within the _posts directory.

To achieve that, define a [slug].ts file inside pages as below:

// pages/[slug].tsx

import fs from "fs"
import { GetStaticPropsContext, InferGetStaticPropsType } from "next"
import { serialize } from "next-mdx-remote/serialize"
import { MDXRemote } from "next-mdx-remote"
import Head from "next/head"
import H1 from "@/components/mdx/H1"
import HeroImage from "@/components/mdx/HeroImage"
import React from "react"
import P from "@/components/mdx/P"
import H2 from "@/components/mdx/H2"

export default function PostPage({ source }: InferGetStaticPropsType<typeof getStaticProps>) {
  return (
    <div>
      <Head>
        <title>{source.frontmatter.title as string}</title>
      </Head>
      <MDXRemote
        {...source}
        // specifying the custom MDX components
        components={{
          h1: H1,
          h2: H2,
          p: P,
          HeroImage,
        }}
      />
    </div>
  )
}
export async function getStaticPaths() {
  return { paths: [], fallback: "blocking" }
}

export async function getStaticProps(
  ctx: GetStaticPropsContext<{
    slug: string
  }>,
) {
  const { slug } = ctx.params!

  // retrieve the MDX blog post file associated
  // with the specified slug parameter
  const postFile = fs.readFileSync(`_posts/${slug}.mdx`)

  // read the MDX serialized content along with the frontmatter
  // from the .mdx blog post file
  const mdxSource = await serialize(postFile, { parseFrontmatter: true })
  return {
    props: {
      source: mdxSource,
    },
    // enable ISR
    revalidate: 60,
  }
}Code language: JavaScript (javascript)

The slug parameter read from the page URL by Next.js is passed to getStaticProps(), where it is used to load the corresponding _posts\[slug].mdx file. Then, its MDX content gets converted into JSX by the serialize() function exposed by next-mdx-remote. Note the parseFrontmatter config flag set to true to parse also the frontmatter contained in the .mdx file.

Finally, the resulting object is passed to the server-side PostPage component. Here, the MDXRemote parser component provided by next-mdx-remote receives the serialized source and the list of custom MDX custom components, using them to render the blog post in HTML.

Since blog posts are likely to be updated over time, you should use the Incremental Static Regeneration (ISR) approach. That is enabled through the revalidate option and allows static pages to be incrementally updated without requiring a complete rebuild of the Next.js app.

Now, suppose you have a top-programming-languages.mdx file inside _posts. Launch your Next.js app and visit the http://localhost:3000/blog/top-programming-languages page in the browser. In this case, slug will contain the "top-programming-languages" string and Next.js will load the desired .mdx file.

This is what Next.js will produce:

The image domain Next.js error

This error occurs because HeroImage tries to display an image coming from Unsplash, one the most popular image provider. By default, Next.js’s API blocks external domains for security reasons. Configure Next.js to work with Unsplash by updating the next.config.js file with the following:

// next.config.jsx

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  images: {
    domains: ["source.unsplash.com"],
  },
}

module.exports = nextConfigCode language: JavaScript (javascript)

Restart the development server and visit http://localhost:3000/blog/top-programming-languages again. This time, you should see your MDX-based blog post:

An MDX blog post rendered by next-mdx-remote

Fantastic! It does not (yet) look good, but it works!

Add a homepage
Your blog needs a fancy homepage containing the latest blog posts. Next.js stores the homepage of your site in pages/index.tsx. Replace that file with the following code:

// pages/index.tsx

import PostCard from "@/components/PostCard"
import { InferGetStaticPropsType } from "next"
import fs from "fs"
import { serialize } from "next-mdx-remote/serialize"
import path from "path"
import { PostPreview } from "@/types/posts"

export default function Home({ postPreviews }: InferGetStaticPropsType<typeof getStaticProps>) {
  return (
    <div>
      {postPreviews.map((postPreview, i) => {
        return (
          <div key={i}>
            <PostCard postPreview={postPreview} />
          </div>
        )
      })}
    </div>
  )
}

export async function getStaticProps() {
  // get all MDX files
  const postFilePaths = fs.readdirSync("_posts").filter((postFilePath) => {
    return path.extname(postFilePath).toLowerCase() === ".mdx"
  })

  const postPreviews: PostPreview[] = []

  // read the frontmatter for each file
  for (const postFilePath of postFilePaths) {
    const postFile = fs.readFileSync(`_posts/${postFilePath}`, "utf8")

    // serialize the MDX content to a React-compatible format
    // and parse the frontmatter
    const serializedPost = await serialize(postFile, {
      parseFrontmatter: true,
    })

    postPreviews.push({
      ...serializedPost.frontmatter,
      // add the slug to the frontmatter info
      slug: postFilePath.replace(".mdx", ""),
    } as PostPreview)
  }

  return {
    props: {
      postPreviews: postPreviews,
    },
    // enable ISR
    revalidate: 60,
  }
}Code language: JavaScript (javascript)

getStaticProps() takes care of loading all .mdx files, transforming them into PostPreview objects, and rendering them in PostCard components.

This is what the PostPreview TypeScript type looks like:

// types/posts.tsx

export type PostPreview = {
  title: string
  description: string
  previewImage: string
  slug: string
}Code language: JavaScript (javascript)

And this is how the PostCard is defined:

import Link from "next/link"
import { PostPreview } from "@/types/posts"

export default function PostCard({ postPreview }: { postPreview: PostPreview }) {
  return (
    <div className={"post-card"} style={{ backgroundImage: `url(${postPreview.previewImage})` }}>
      <Link href={postPreview.slug}>
        <div className={"post-card-content"}>
          <h2 className={"post-card-title"}>{postPreview.title}</h2>
          <p className={"post-card-description"}>{postPreview.description}</p>
        </div>
      </Link>
    </div>
  )
}Code language: JavaScript (javascript)

If you style PostCard with some CSS rules, http://localhost:3000 should appear as in the image below:

The MDX blog’s homepage

Well done! It only remains to add a layout to your blog and style it accordingly!

Style your blog
The main problem with your blog right now is that it takes up the entire viewport width. You can avoid that with a Bootstrap container. Install bootstrap with:

npm install bootstrap

Then, add the following line to _app.tsx:

import "bootstrap/dist/css/bootstrap.css"Code language: JavaScript (javascript)

Now, define a Next.js layout based on Bootstrap:

// components/Layout.tsx

import React from "react"
import Header from "@/components/Header"
import { Inter } from "next/font/google"

const inter = Inter({ subsets: ["latin"] })

export default function Layout({ children }: { children?: React.ReactNode }) {
  return (
    <div className={inter.className}>
      <Header />
      <div className={"container"}>{children}</div>
    </div>
  )
}Code language: JavaScript (javascript)

Take advantage of Next.js font API to set a good-looking Google font for your blog. Also, wrap the entire content under the Header component with a Bootstrap .container div.

If you are wondering, Header is nothing more than a simple div containing the title of your blog:

// components/Header.tsx

import { Nunito } from "next/font/google"
import Link from "next/link"

const nunito = Nunito({ subsets: ["latin"] })

export default function Header() {
  return (
    <div className={`header mb-4 ${nunito.className}`}>
      <div className={"container"}>
        <div className={"header-title mt-4"}>
          <Link href="/">My MDX Blog in Next.js</Link>
        </div>
      </div>
    </div>
  )
}Code language: JavaScript (javascript)

Now, wraps the Component instance containing all your Next.js site with Layout:

// _app.tsx

import "@/styles/globals.scss"
import type { AppProps } from "next/app"
import "bootstrap/dist/css/bootstrap.css"
import Layout from "@/components/Layout"
export default function App({ Component, pageProps }: AppProps) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  )
}Code language: JavaScript (javascript)

As you can see, the global CSS file styles/globals.scss imported here is a SASS file. SCSS is much more powerful than CSS and makes it easier to style your web application. Next.js supports SCSS after installing sass with:

npm install sass

Make your Next.js MDX TypeScript blog look original and eye-catching with some SCSS rules!

Put it all together
You can find the entire code of the MDX-based blog developed in TypeScript with Next.js in the GitHub repository that supports the article. Clone it and launch the blog locally with:

git clone https://github.com/Tonel/nextjs-mdx-typescript-blog
cd nextjs-mdx-typescript-blog
npm i
npm run devCode language: PHP (php)

Visit htpp://localhost:3000 in your browser and explore the MDX Next.js blog.

Conclusion

In this step-by-step guide, you saw how to create an MDX blog with TypeScript and Next.js. With the help of MDX, you can easily mix and match React components with Markdown content, making it easy to create rich and engaging blog posts.

One of the best things about using MDX with Next.js is that you have several options for managing your blog’s content. You can use a headless CMS to manage your MDX content, or you can update .mdx files directly on GitHub. Regardless of which approach you choose, MDX will allow you to create the blog post that fits your needs.

Thanks for reading! We hope you found this article helpful!


More about TypeScript here:
Why you should use Typescript for your next project
Typescript 10 years after release

Related Posts

Understanding Angular — Exploring Dependency Injection and Design Patterns — Part 0 🔥🚀

Giorgio Galassi
February 5, 2025

Let’s Create a Bento Box Design Layout Using Modern CSS

Massimo Avvisati
January 21, 2025
React library: all you need to know about it.

Building reusable multiple-step form in ReactJS

Noa Shtang
August 8, 2024
excalidraw codemotion magazine

Excalidraw: Diagrams Set in Stone

TheZal
July 9, 2024
Share on:facebooktwitterlinkedinreddit

Tagged as:NextJS typescript

Antonello Zanini
I'm a software engineer, but I prefer to call myself a Technology Bishop. Spreading knowledge through writing is my mission.
Anonymous Survey: Is AI Changing How Developers Work?
Previous Post
Data-Centric AI: The Key to Unlocking the Full Potential of Machine Learning
Next Post

Footer

Discover

  • Events
  • Community
  • Partners
  • Become a partner
  • Hackathons

Magazine

  • Tech articles

Talent

  • Discover talent
  • Jobs

Companies

  • Discover companies

For Business

  • Codemotion for companies

About

  • About us
  • Become a contributor
  • Work with us
  • Contact us

Follow Us

© Copyright Codemotion srl Via Marsala, 29/H, 00185 Roma P.IVA 12392791005 | Privacy policy | Terms and conditions