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.
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 3
Code 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 --ts
Code 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:
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:
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 = nextConfig
Code 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:
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.ts
x. 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:
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 dev
Code 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