TIL: how to display markdown files in Next.js
This article is part of a new ”Today I learned” series, where I share a thing I learned that you might find useful too
I want to learn how to use Next.js’ app router because I’m about to start using it in anger at work and I want to be ready. So I started thinking about how I would rewrite this blog in it. I might end up using the end result, I might not, we’ll see - it’s just a good practice opportunity more than anything.
Hashnode lets me export all my articles as markdown files, so it makes sense to see whether Next.js can display markdown. Turns out, it can and it’s super easy! Let me show you how…
Create a new app router project
If you’ve already got one, you can skip this step. If not, do the following:
-
Open your terminal and type
npx create-next-app -
Vercel’s
create-next-apputility will ask you a bunch of questions:-
What is your project name? enter a name, or leave blank for the
my-appdefault -
Would you like to use the recommended Next.js defaults? you can say yes to this but I always say no because I like having control, so if you want to do that too, select “No, customize settings”
-
Would you like to use TypeScript? select yes (we shouldn’t be using JavaScript for new projects in this day and age)
-
Which linter would you like to use? your preference here; I selected Biome because I’ve not used it before and I’d like to try it out
-
Would you like to use React Compiler? I selected “no” because a blog is very static and won’t require memoization
-
Would you like to use Tailwind CSS? I selected “no” but it’s not relevant for this example so feel free to choose whatever you’d like
-
Would you like your code inside a
src/directory? I said “yes” here because it’s what I’m used to -
Would you like to use App Router? this is the biggie - select “yes” here
-
Would you like to customize the import alias (`@/*` by default)? select “no” - it’s not important for our example
-
-
Your project should be created in a directory matching the name of your app 🎉
-
Open your new shiny app in your favourite IDE
Install the markdown dependencies
We need to install @mdx-js/loader (Webpack loader), @next/mdx (Next.js plugin for MDX) and @types/mdx (type definitions, so this works with TypeScript).
Here’s the install command for those modules - just copy/paste this into your VS Code terminal and hit enter:
npm install @mdx-js/loader @next/mdx @types/mdx
The Vercel documentation says you should install
@mdx-js/reactas well, but the NPM page for this module says it’s not needed, so let’s not add an unnecessary dependency!
Update the Next.js config
We need to update our Next.js config to display markdown files:
// Import the Next.js markdown plugin
import createMDX from "@next/mdx";
/** @type {import('next').NextConfig} */
const nextConfig = {
// Add the .md and .mdx extensions so Next.js can handle them
pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
};
// Make sure the markdown plugin runs on .md and .mdx files
const withMDX = createMDX({
extension: /\.(md|mdx)$/,
});
// Add the MDX config to your Next.js config
export default withMDX(nextConfig);
Simple! Onto the next step.
Add an mdx-components.tsx file
In your application root (if you opted to have a src directory, it’ll go in there), add a file called mdx-components.tsx with the following contents:
import type { MDXComponents } from 'mdx/types'
const components: MDXComponents = {}
export function useMDXComponents(): MDXComponents {
return components
}
This defines a function useMDXComponents that will render all the content in your markdown files, converting it to HTML as it goes.
This file will let you do things like replace HTML elements with React components or add classnames to the auto-generated HTML elements, but we don’t need to worry about that for now.
Put your markdown files in the /app directory
Now that all the setup is done, you can add your markdown files to the /app directory - I grabbed the very first post I ever did on this blog and put it in an introduction folder - you’ll have to name the markdown file page.md to match the app router’s filename rules. So in my case, my markdown file lives at /src/app/introduction/page.md.
Now run your local dev server by running npm run dev and go to http://localhost:3000/introduction (or wherever you put your file) and you should see your markdown file in all its glory:

It’s not pretty, because it needs styling etc, but it works! How cool is that?
