Anthony Di Biaggio
Augmenting Next.js Blog Starter with MDX
Markdown is cool, but I thought it would be nice to add more potential for interactivity to my blog posts without making them more complicated to write, so I explored extending my current setup with MDX. MDX is a superset of Markdown that gives it JSX support.
This is pretty powerful because it allows you to put anything you could build in React into a Markdown file. The Markdown gets converted to static HTML like usual on the server side, and then any React components in Markdown files are hydrated on the client side. You can also pass in a component mapping to the MDX provider:
MDXRemote component mapping
1const components = {2img: NextImage,3Button: Button,4pre: ({ children, className }) => {5return <pre>{children}</pre>6},7code: ({ children, className }) => {8return className ? (9<CodeBlock className={className}>{children}</CodeBlock>10) : (11<code className="language-text">{children}</code>12)13},14}
which basically will take any JSX tags on the left from MDX files and replace them with those on the right.
This means you can take Markdown elements and replace them with something richer.
For example, the base <code>
element in Markdown doesn't do very sophisticated syntax highlighting, and
looks kind of boring. You may have noticed that in the above component mapping I have <code>
tags being
replaced with a CodeBlock component:
Component for syntax highlighting
1const LineNumber = (props) => (2<Box3color={'accent'}4paddingRight={'0.75rem'}5userSelect={'none'}6display={'table-cell'}7>8{props.number}9</Box>10)1112const CodeBlock = ({ children, className }) => {13const language = className ? className.replace(/language-/, '') : 'javascript'1415return (16<Highlight17{...defaultProps}18code={children}19language={language}20theme={theme}21>22{({ className, style, tokens, getLineProps, getTokenProps }) => (23<Box24as={'pre'}25className={className}26style={{27...style,28padding: '10px',29marginBottom: '32px',30overflow: 'scroll',31marginLeft: '-70px',32marginRight: '-70px',33textAlign: 'left',34}}35>36{tokens.map((line, i) => (37<Box38verticalAlign={'center'}39display={'table-row'}40key={i}41{...getLineProps({ line, key: i })}42>43<LineNumber number={i + 1} />44{line.map((token, key) => (45<Box as={'span'} key={key} {...getTokenProps({ token, key })} />46))}47</Box>48))}49</Box>50)}51</Highlight>52)53}
which uses prism-react-renderer
to render a more interesting environment for code to live in.
It looks messy, but the way prism-react-renderer works gives a lot of flexibility with how you
want your code blocks to look. I was able to add line numbers that match the accent color of my
site just by throwing in a component.
And, since all Markdown <code>
elements are replaced with this CodeBlock component, I can keep
using the easy Markdown syntax for writing code.
If you want to do this with your Next.js blog, it's actually pretty easy. Just npm i next-mdx-remote
,
import the MDXRemote
component,
MDXRemote import
1import { MDXRemote } from 'next-mdx-remote'
and then place MDXRemote
wherever you want your compiled MDX to show up. For my site, I have a PostBody
element that represents the content of each blog post, so I put it there:
PostBody component
1const PostBody = ({ content }) => {2return (3<Box maxW={'2xl'} mx={'auto'}>4<Box className={markdownStyles['markdown']}>5<MDXRemote {...content} components={components} lazy />6</Box>7</Box>8)9}
content
is just the return value of next-mdx-remote
's serialize function, which takes in an MDX
string and performs the compilation:
Serializing markdown
1import { serialize } from 'next-mdx-remote/serialize'23const fileContents = fs.readFileSync(fullPath, 'utf8')4const content = await serialize(fileContents, { parseFrontmatter: true })5const data = content.frontmatter
I am using frontmatter
, so I passed an additional option to serialize
, but you can just leave that out
and only pass the file contents. It's really that simple - as long as you feed serialize
's output into
the client side MDXRemote
component, it just works. And then you can customize the components
that will
show up in your Markdown however you like - heck, replace <pre><code>
with a full-fledged development
environment if you want. You can even embed a THREE.js
canvas with react-three-fiber
:
react-three-fiber example
1function SpinnyCube(props) {2const mesh = useRef()3const [hovered, setHovered] = useState(false)4const [active, setActive] = useState(false)5const [accentColor, setAccentColor] = useState('')6const { colorMode, toggleColorMode } = useColorMode()78useEffect(() => {9setAccentColor(getComputedStyle(document.body).getPropertyValue('--accent'))10}, [colorMode])1112useFrame((state, delta) => {13mesh.current.rotation.x += delta14mesh.current.rotation.z += delta15})1617return (18<mesh19{...props}20ref={mesh}21scale={active ? 1 : 1.2}22onClick={(event) => setActive(!active)}23onPointerOver={(event) => setHovered(true)}24onPointerOut={(event) => setHovered(false)}25>26<boxGeometry args={[2, 2, 2]} />27<meshStandardMaterial color={hovered ? 'darkgreen' : accentColor} />28</mesh>29)30}3132export default function ReactThreeFiberExample(props) {33return (34<Canvas>35<ambientLight />36<pointLight position={[10, 10, 10]} />37<SpinnyCube position={[-3, 0.5, 0]} />38<SpinnyCube position={[3, -0.7, 0]} />39</Canvas>40)41}
Cool.