Building your own blog article site and rendering Markdown content with Next.JS isn't difficult - there are a lot of different ways to get the job done. For Serverless DNA we opted to use the react-markdown package which utilises a combination of remark and rehype to process and render the Markdown content into HTML. This provides a good outcome and also provides some control over how particular aspects of Markdown can be rendered. In our builder journey, we found the documentation and examples of customisation for react-markdown to be a little "Hacky".
One of the customisations we made is in the rendering of code blocks, we wanted to add in the Prism SyntaxHighligher which provides themed rendering of code blocks. Integrating the Prism SyntaxHighlighter component is described in the main github page of the npm package, here. As you can see from the in-line sample the customisation for rendering Markdown is handled in the components attribute, which works when applying a single customisation, but when one or more customisations are needed or planned, the code becomes un-maintainable very quickly.
import React from 'react' import ReactDom from 'react-dom' import ReactMarkdown from 'react-markdown' import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter' import {dark} from 'react-syntax-highlighter/dist/esm/styles/prism' // Did you know you can use tildes instead of backticks for code in markdown? ✨ const markdown = `Here is some JavaScript code: ~~~js console.log('It works!') ~~~ ` ReactDom.render( <ReactMarkdown children={markdown} components={{ code({node, inline, className, children, ...props}) { const match = /language-(\w+)/.exec(className || '') return !inline && match ? ( <SyntaxHighlighter children={String(children).replace(/\n$/, '')} style={dark} language={match[1]} PreTag="div" {...props} /> ) : ( <code className={className} {...props}> {children} </code> ) } }} />, document.body )
The example is also Javascript and not Typescript so we also had to figure out the Typing of our custom components for them to be effective, this was not a simple as we first thought (being new to Typescript is more than likely why this was hard).
We decided to create our own custom Markdown wrapper component so we could customise markdown rendering and not have to expose that to the main blog pages housing our content. Our custom markdown component is shown below. As you can see the outcome of applying customisations is identical to the react-markdown example, except we are not in-lining our code. Instead we have imported our own rendering functions which enable customisation of Markdown rendering. We have applied custom rendering to code, a, img and blockquote. Image and Links we decided to customise so we could render them using the Next.JS components instead of the existing vanilla HTML tags and blockquote we wanted to provide our own look and feel. For the remainder fo this article we will focus on the CodeHighlight component and how we integrated it.
import ReactMarkdown from 'react-markdown'; import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown'; import remarkGfm from 'remark-gfm'; import CodeHighlight from './markdown/codeHighlight'; import Link from './markdown/link'; import Image from './markdown/image'; import Quote from './markdown/quote'; export default function Markdown({ children, className, linkTarget }: ReactMarkdownOptions) { return ( <ReactMarkdown className={className} linkTarget={linkTarget} components={{ code: CodeHighlight, a: Link, img: Image, blockquote: Quote, }} remarkPlugins={ [ remarkGfm, ] } > { children } </ReactMarkdown> ); }
Rendering Code with Syntax Highlighting
Customising redering of the code markdown block you must provide an function implementation to be called by the Markdown component. In this case you can create a function containing the in-line code and needed to hunt down the Type declaration for the function type. To be able to access the properties for the code block we needed to hunt down the CodeProps that are passed to the rendering function, being new to Typescript this took a little trial and error and detective work. We needed to make sure we found the types passed into the render function and not the return type of the function. To find the actual Property type we needed to explore up the type tree using VS Code through clicking on types and exploring up the hierarchy. We started with Our markdown component whiuch took us to the component return type:
export type SpecialComponents = { code: CodeComponent | ReactMarkdownNames ... }
From here you can explore to CodeComponent
export type CodeComponent = ComponentType<CodeProps>
and finally we get to CodeProps - this is the one you need!
export type CodeProps = ComponentPropsWithoutRef<'code'> & ReactMarkdownProps & { inline?: boolean }
With this type we are now able to create the rendering function to provide a clean rendering function that also satisfies the strict es-lint rules setup by the default Next.JS project. To keep things clean create a markdown sub-folder in your components folder and place all custom markdown render functions in this folder. In this way we end up with a clean set of files that are easy to find. Each cusomt function is in it's own file and we can import and use these render customisations quickly and easily.
This is how we managed to provide a clean framework to customise rendering of Markdown for Serverless DNA, we hope you find it useful in your Markdown rendering journey. In the following sections is the code for each of our Markdown customisations which you may find useful in your Next.JS project.
CodeHighlight Render Function
import { CodeProps } from 'react-markdown/lib/ast-to-react'; import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter' import {dracula as codeStyle} from 'react-syntax-highlighter/dist/cjs/styles/prism' export default function CodeHighlight({node, inline, className, children, ...props}: CodeProps) { const match = /language-(\w+)/.exec(className || '') let widget; const classes = `${className} ` if(!inline && match) { widget = ( <SyntaxHighlighter style={codeStyle} language={match[1]} className={classes} PreTag="div" {...props} > {String(children).replace(/\n$/, '')} </SyntaxHighlighter> ); } else { widget = ( <code className={classes} {...props}> {children} </code> ); } return widget; }
Image Render function
import NextImage from 'next/image'; import React from 'react'; type IImageProps = React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>; export default function Link({src,className, alt, children}: IImageProps) { className = `${className} img-fluid` return ( <> <NextImage src={src || ''} className={className} width={800} height={500} alt={alt || 'un-named blog article Image'} > </NextImage> </> ) }
Link Render function
import NextLink from 'next/link'; import { ReactNode } from 'react'; import React from 'react'; import { ComponentPropsWithoutRef } from 'react-markdown/lib/ast-to-react'; type ILinkProps = React.DetailedHTMLProps<React.AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>; export default function Link({href,className, children}: ILinkProps) { return ( <NextLink href={href||''} className={className} target="_blank" > {children} </NextLink> ) }
Quote Render function
import React from 'react'; type IQuoteProps = React.DetailedHTMLProps<React.BlockquoteHTMLAttributes<HTMLQuoteElement>, HTMLQuoteElement>; export default function Quote({className, children}: IQuoteProps) { className = `${className||''} p-4 mx-auto shadow rounded-4 w-75 text-center align-middle bg-light` return ( <> <div className="my-4"> <div className={className} > <blockquote className='blockquote'> <em> {children} </em> </blockquote> </div> </div> </> ) }