Accountable Markdown in Subsequent.js

No Comments

Markdown actually is a good format. It’s shut sufficient to plain textual content in order that anybody can rapidly study it, and it’s structured sufficient that it may be parsed and ultimately transformed to you title it.

That being stated: parsing, processing, enhancing, and changing Markdown wants code. Transport all that code within the consumer comes at a value. It’s not large per se, nevertheless it’s nonetheless a number of dozens of kilobytes of code which are used solely to cope with Markdown and nothing else.

On this article, I need to clarify find out how to hold Markdown out of the consumer in a Subsequent.js software, utilizing the Unified/Comment ecosystem (genuinely unsure which title to make use of, that is all tremendous complicated).

Common concept

The thought is to solely use Markdown within the getStaticProps capabilities from Subsequent.js so that is executed throughout a construct (or in a Subsequent serverless operate if utilizing Vercel’s incremental builds), however by no means within the consumer. I assume getServerSideProps would even be superb, however I believe getStaticProps is extra more likely to be the frequent use case.

This might return an AST (Summary Syntax Tree, which is to say a giant nested object describing our content material) ensuing from parsing and processing the Markdown content material, and the consumer would solely be liable for rendering that AST into React elements.

I assume we might even render the Markdown as HTML instantly in getStaticProps and return that to render with dangerouslySetInnerHtml however we’re not that type of individuals. Safety issues. And in addition, flexibility of rendering Markdown the best way we would like with our elements as a substitute of it rendering as plain HTML. Critically people, don’t try this. 😅

export const getStaticProps = async () => {
// Get the Markdown content material from someplace, like a CMS or whatnot. It doesn’t
// matter for the sake of this text, actually. It is also learn from a
// file.
const markdown = await getMarkdownContentFromSomewhere()
const ast = parseMarkdown(markdown)

return { props: { ast } }
}

const Web page = props => {
// This might normally have your structure and whatnot as effectively, however omitted right here
// for sake of simplicity in fact.
return <MarkdownRenderer ast={props.ast} />
}

export default Web page

Parsing Markdown

We’re going to use the Unified/Comment ecosystem. We have to set up unified and remark-parse and that’s about it. Parsing the Markdown itself is comparatively easy:

import unified from ‘unified’
import markdown from ‘remark-parse’

const parseMarkdown = content material => unified().use(markdown).parse(content material)

export default parseMarkdown

Now, what took me an extended whereas to grasp is why my additional plugins, like remark-prism or remark-slug, didn’t work like this. It’s because the .parse(..) methodology from Unified doesn’t course of the AST with plugins. Because the title suggests, it solely parses the string of Markdown content material right into a tree.

If we would like Unified to use our plugins, we want Unified to undergo what they name the “run” part. Usually, that is executed by utilizing the .course of(..) methodology as a substitute of the .parse(..) methodology. Sadly, .course of(..) not solely parses Markdown and applies plugins, but in addition stringifies the AST into one other format (like HTML through remark-html, or JSX with remark-react). And this isn’t what we would like, as we need to protect the AST, however after it’s been processed by plugins.

| …………………… course of ……………………… |
| ………. parse … | … run … | … stringify ……….|

+——–+ +———-+
Enter ->- | Parser | ->- Syntax Tree ->- | Compiler | ->- Output
+——–+ | +———-+
X
|
+————–+
| Transformers |
+————–+

So what we have to do is run each the parsing and operating phases, however not the stringifying part. Unified doesn’t present a technique to do these 2 out of three phases, nevertheless it offers particular person strategies for each part, so we are able to do it manually:

import unified from ‘unified’
import markdown from ‘remark-parse’
import prism from ‘remark-prism’

const parseMarkdown = content material => {
const engine = unified().use(markdown).use(prism)
const ast = engine.parse(content material)

// Unified‘s *course of* accommodates 3 distinct phases: parsing, operating and
// stringifying. We don’t need to undergo the stringifying part, since we
// need to protect an AST, so we can not name `.course of(..)`. Calling
// `.parse(..)` just isn’t sufficient although as plugins (so Prism) are executed throughout
// the operating part. So we have to manually name the run part (synchronously
// for simplicity).
// See: https://github.com/unifiedjs/unified#description
return engine.runSync(ast)
}

Tada! We parsed our Markdown right into a syntax tree. After which we ran our plugins on that tree (executed right here synchronously for sake of simplicity, however you might use .run(..) to do it asynchronously). However we didn’t convert our tree into another syntax like HTML or JSX. We will try this ourselves, within the render.

Rendering Markdown

Now that we now have our cool tree on the prepared, we are able to render it the best way we intend to. Let’s have a MarkdownRenderer element that receives the tree as an ast prop, and renders all of it with React elements.

const getComponent = node => {
swap (node.kind) {
case ‘root’:
return React.Fragment

case ‘paragraph’:
return ‘p’

case ’emphasis’:
return ’em’

case ‘heading’:
return ({ kids, depth = 2 }) => {
const Heading = `h${depth}`
return <Heading>{kids}</Heading>
}

/* Deal with every kind right here … */

default:
console.log(‘Unhandled node kind’, node)
return React.Fragment
}
}

const Node = node => {
const Part = getComponent(node)
const { kids } = node

return kids ? (
<Part {…node}>
{kids.map((youngster, index) => (
<Node key={index} {…youngster} />
))}
</Part>
) : (
<Part {…node} />
)
}

const MarkdownRenderer = props => <Node {…props.ast} />

export default React.memo(MarkdownRenderer)

A lot of the logic of our renderer lives within the Node element. It finds out what to render primarily based on the kind key of the AST node (that is our getComponent methodology dealing with each kind of node), after which renders it. If the node has kids, it recursively goes into the youngsters; in any other case it simply renders the element as a remaining leaf.

Cleansing up the tree

Relying on which Comment plugins we use, we would encounter the next downside when making an attempt to render our web page:

Error: Error serializing .content material[0].content material.kids[3].information.hChildren[0].information.hChildren[0].information.hChildren[0].information.hChildren[0].information.hName returned from getStaticProps in “/”. Cause: undefined can’t be serialized as JSON. Please use null or omit this worth.

This occurs as a result of our AST accommodates keys whose values are undefined, which isn’t one thing that may be safely serialized as JSON. Subsequent offers us the answer: both we omit the worth totally, or if we want it considerably, exchange it with null.

We’re not going to repair each path by hand although, so we have to stroll that AST recursively and clear it up. I discovered that this occurred when utilizing remark-prism, a plugin to allow syntax highlighting for code blocks. The plugin certainly provides a [data] object to nodes.

What we are able to do is stroll our AST earlier than returning it to scrub up these nodes:

const cleanNode = node => {
if (node.worth === undefined) delete node.worth
if (node.tagName === undefined) delete node.tagName
if (node.information) {
delete node.information.hName
delete node.information.hChildren
delete node.information.hProperties
}

if (node.kids) node.kids.forEach(cleanNode)

return node
}

const parseMarkdown = content material => {
const engine = unified().use(markdown).use(prism)
const ast = engine.parse(content material)
const processedAst = engine.runSync(parsed)

cleanNode(processedAst)

return processedAst
}

One last item we are able to do to ship much less information to the consumer is take away the place object which exists on each single node and holds the unique place within the Markdown string. It’s not a giant object (it has solely two keys), however when the tree will get huge, it provides up rapidly.

const cleanNode = node => {
delete node.place

Wrapping up

That’s it people! We managed to limit Markdown dealing with to the build-/server-side code so we don’t ship a Markdown runtime to the browser, which is unnecessarily pricey. We go a tree of information to the consumer, which we are able to stroll and convert into no matter React elements we would like.

I hope this helps. 🙂

The put up Accountable Markdown in Subsequent.js appeared first on CSS-Methods. You possibly can help CSS-Methods by being an MVP Supporter.

    About Marketing Solution Australia

    We are a digital marketing company with a focus on helping our customers achieve great results across several key areas.

    Request a free quote

    We offer professional SEO services that help websites increase their organic search score drastically in order to compete for the highest rankings even when it comes to highly competitive keywords.

    Subscribe to our newsletter!

    More from our blog

    See all posts

    Leave a Comment