Markdown allows text content to be written with implicit formatting that nearly matches how one would write text without thought of formatting. A lot of web content is also simply text content; a lot of web content is built on the ReactJS library; and NextJS has become a popular framework, extending ReactJS. With that, I was motivated to use the Markdown content of my NextJS web app, but I had trouble simply relying on their documentation, so I thought I would document what I got working.
- Use Markdown for a text-heavy portion of a page of my web app, and
- To be able to have an entire page of content come from a single Markdown page
The former is a feature of NextJS to allow markdown content to be imported as a “component,” which can be rendered like any component in ReactJs.
The latter would rely NextJS’s App Router that assumes many URL paths for the app parallel the file/directory hierarchy of the app itself. So, merely placing a “page” file in the app’s directory will create a new URL path for the website. That means that you can create a “page.md” or “page.mdx” file with Markdown text content to define the content for that URL. .mdx files also allow JSX syntax so that React/Next components can be embedded within the Markdown.
Setup
The first issue was getting things set up. Here were the steps that worked for me.
- Add packages. The highlighted line was missing from the documentation.
npm install @mdx-js/loader @mdx-js/react @next/mdx npm install -D @types/mdx
- In
next.config.*
, addimport withMDX from "@next/mdx";
- Add “md” (optional) and “mdx” extensions for App Router support:
nextConfig.pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx']
- Create, then wrap the Next config with mdx-config
withMDX = createMDX({})
…export default withMDX(nextConfig)
Note: I had to avoid using
.ts
to avoid syntax errors to support the “remark-gfm” plugin. I recommend using.js
or.mjs
extensions, I chose the latter.import type {NextConfig} from 'next'; const nextConfig: NextConfig = { typescript: { ignoreBuildErrors: true, }, eslint: { ignoreDuringBuilds: true, }, }; export default nextConfig;
asdfasdf asdf asdf asd fimport type {NextConfig} from 'next'; import withMDX from "@next/mdx"; const nextConfig: NextConfig = { pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"], typescript: { ignoreBuildErrors: true, }, eslint: { ignoreDuringBuilds: true, }, }; const withMdxConfig = withMDX({ options: { remarkPlugins: [], rehypePlugins: [], }, }); export default withMdxConfig(nextConfig);
- Add the mdx-components.tsx file to
src/
. This file allows overriding how tags and components will be rendered, providing a mapping from tag/component name to an overriding render function; see the official documentation, mdx-components.js, but I will elaborate on how we’ll use this in the following sections.import type { MDXComponents } from 'mdx/types' export function useMDXComponents(components: MDXComponents): MDXComponents { return { ...components, } }
- Help VSCode resolve imports
- Open settings
- Search for “file associations”
- Add item key =
*.mdx
, value =markdown
. (Articles say, “react-markdown,” but that didn’t work for me).
- Styles will need to be defined for all the generated HTML tags. See the next sections.
Defining Markdown Output Style
Markdown syntax is translated to HTML. By default, the HTML tags have had their formatting stripped, for some reason. That means that the tags need to have style/class definitions applied to render properly. See How to use markdown and MDX in Next.js, for the official word.
The following are tags that correspond to Markdown syntax:
Markdown | HTML | |
---|---|---|
Headings | # Level 1## Level 2### Level 3… ###### Level 6 |
<h1> Level 1</h1> <h2> Level 2</h2> <h3> Level 3</h3> … <h6> Level 6</h6> |
Bold, Italic, Bold Italic | * and _ are interchangeable** bold** or __ bold__ * italic* or _ italic_ *** bold-italic*** |
<b> bold</b> <i> italic</i> <b><i> bold-italic</i></b> or <i><b> bold-italic</b></i> |
Bullet list | - Item 1- Item 2 |
<ul> <li> Item 1</li> <li> Item 2</li> </ul> |
Ordered list | 1. Item 12. Item 2 |
<ol> <li> Item 1</li> <li> Item 2</li> </ol> |
Unformatted |
|
|
Link | [ description]( http://…) |
<a href=" http://…"> description</a> |
Plain or paragraph text | text … | <p> text …</p> |
Checkbox (an extended feature supported by the remark-gfm plugin) |
|
|
By default, NextJS removes the styling for the Markdown HTML tags, so we have to define them.
Wrapper
Styles and classnames can be defined for any tag and component by redefining it to render with a style
or className
property, as desired. Instead, we will take advantage of a special Component named “wrapper” that wraps any .mdx
content and define tag styles within that container context. Then we define all tag’s styles in an importable CSS module.
import type { MDXComponents } from 'mdx/types' import styles from './mdx-components.module.css' export function useMDXComponents(components: MDXComponents): MDXComponents { return { ...components, wrapper: ({ children }) => <div className={styles.mdxWrapper}>{children}</div>, } }
The file name and the wrapper class names are irrelevant so long as they match. Here is a good starting point for the “mdx-components.module.css” that I use. Note the class name, “_mdxWrapper” parent style definition.
/** * Style definitions for tags corresponding to Markdown for MDX components * This file provides a reset for common HTML elements used in MDX content * to ensure consistent styling. */ ._mdxWrapper { h1, h2, h3, h4, h5, h6, p, blockquote, pre, table { display: block; margin-block: initial; margin-inline: initial; } h1, h2, h3, h4, h5, h6 { font-weight: bold; } h1 { border-top: 1px solid lightgray; margin-block-start: 0.67em; font-size: 2em; } h2 { margin-block-start: 0.83em; font-size: 1.5em; } h3 { margin-block-start: 1em; font-size: 1.17em; } h4 { margin-block-start: 1.67em; font-size: 1em; } h5 { margin-block-start: 1.33em; font-size: 0.83em; } h6 { margin-block-start: 2.33em; font-size: 0.67em; } p { margin-block: 0.4em; } a { color: -webkit-link; text-decoration: underline; cursor: pointer; } ul, ol { display: block; padding-inline-start: 1.5625rem; } ul { list-style-type: disc; } ul ul { list-style-type: circle; } ul ul ul { list-style-type: square; } ol { list-style-type: decimal; } blockquote { margin-block: 1em; margin-inline: 40px; } code, pre { font-family: monospace; } pre { display: block; margin-block: 1em; white-space: pre; } img { display: inline-block; max-width: 100%; height: auto; } table { /* Table styling variables */ /* --table-border-color: blue; */ /* --table-border-width: 2px; */ /* Enable to non-0 to surround table with border */ /* --table-header-background-color: #f2f2f2; */ /* Enable for distinct header background color */ --table-header-border-color: #BBB; /* Defaults to cell border color */ --table-header-border-width: 2px; /* Defaults to row border width */ --table-body-row-border-width: 1px; /* Enable to non-0 show row dividers */ --table-body-col-border-width: var(--table-body-row-border-width,1px); /* Enable to non-0 show col dividers */ --table-body-cell-border-color: lightgray; /* Override border color */ --table-body-background-color: white;/* Even row background color */ --table-body-background-color-alt: #f9f9f9; /* Odd row background color */ margin-left: var(--table-body-row-border-width,0); border-collapse: collapse; text-indent: initial; } thead, tbody, tfoot { vertical-align: middle; } thead { border-color: var(--table-border-color); border-width: var(--table-border-width,0) var(--table-border-width,0) 0 var(--table-border-width,0); } tbody { border-color: var(--table-border-color); border-width: 0 var(--table-border-width,0) var(--table-border-width,0) var(--table-border-width,0); } thead th { background-color: var(--table-header-background-color, inherit); /* border-style: solid; */ border-color: var(--table-header-border-color, var(--table-body-cell-border-color)); border-bottom-width: var(--table-header-border-width,var(--table-body-row-border-width,0)); } tr { vertical-align: inherit; border: none; /* Disable row borders */ border-color: inherit; } td, th { /* No top border, conditional side/bottom borders */ border-width: 0 var(--table-body-col-border-width, 0px) var(--table-body-row-border-width, 0px) var(--table-body-col-border-width, 0px); border-color: var(--table-body-cell-border-color); padding-inline: 8px; } /* No side borders */ td:last-child, th:last-child { border-right: none; } td:first-child, th:first-child { border-left: none; } /* Alternating background colors for table rows */ tbody tr:nth-child(odd) { background-color: var(--table-body-background-color); } tbody tr:nth-child(even) { background-color: var(--table-body-background-color-alt); } } /* ._mdxWrapper */
Checkbox Support via “remark-gfm”
Github markup is supported via the remark-gfm plugin, however I was never able to get it to work as documented (i.e., import and add remarkGfm
to the plugins array. Instead I had to use the string name of the plugin and add an array to the plugins array:
const withMdxConfig = withMDX({ options: { remarkPlugins: [['remark-gfm']], rehypePlugins: [], }, }); export default withMdxConfig(nextConfig);
import withMDX from "@next/mdx" import remarkGfm from 'remark-gfm' const nextConfig: NextConfig = { pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"], } const withMdxConfig = withMDX({ options: { // If you use remark-gfm, you'll need to use next.config.mjs remarkPlugins: [remarkGfm], rehypePlugins: [], }, }) export default withMdxConfig(nextConfig);
The styling for task/checkboxes needs to be set. Unfortunately, remark-gfm uses class names to identify the list and list item tags. Since importing a CSS modules mangles names, we have to import the CSS definitions directly. We can create a CSS file, randomly named, “remark-gfm.css”
/* remarkGfm MDX Checkbox support for remarkGfm */ ul.contains-task-list li.task-list-item { list-style-type: none; /* Remove list style for task list items */ text-indent: -18px; /* ...and outdent it so it aligns */ } /* Undo <li> inherited styling within nested chckbox lists */ ul.contains-task-list li:not(.task-list-item) { list-style-type: initial; text-indent: initial; }
Then import this directly into “mdx-components.tsx.” Alternatively you can add these style definitions in global.css.
import type { MDXComponents } from 'mdx/types' import styles from './mdx-components.module.css' import './remark-gfm.css' export function useMDXComponents(components: MDXComponents): MDXComponents { return { ...components, wrapper: ({ children }) => <div className={styles._mdxWrapper}>{children}</div>, } }
Note: This style definition will affect all content on the page, even outside an MDX component.
Summary
I was led down a bunch of rabbit holes due to NextJS’s Markdown support not matching the documented steps. With the steps described in this post, others should be able to get it working without the headaches I had to go through.
- Npm-install mdx support packages
- Update VSCode file association settings (optionally)
- Update “next.config.mjs”
- Create src/mdx-components.tsx and import a CSS module to define and wrap tag style definitions
- If Github markdown support is desired, add the plugin and import CSS definitions into mdx-components.tsx
Then you can use Markdown content for App Router pages as page.mdx
and React components, implicitly.
# Page Title This is an example a NextJS App Router page, written with Markdown syntax.
import UpdateContent from "./updates.mdx" // Markdown content export default async function UpdatesPage() { // App Router page component return ( <> <h1>Updated News</h1> <UpdateContent/> {/* Render Markdown component */} </> )