Customize Netlify CMS preview with Markdown-it and Prism.js
This site is hosted on Netlify and configured with Netlify CMS. I normally would like to write my post or other contents on this site using vim or other text editors. However, sometimes it is convenient to be able to edit contents online (in the browser) and a CMS allows me to do just that. I can just login https://www.shisguang.com/admin/ in any computer and start editing. In addition, a CMS provides UI to easier editing. The post written and saved in the admin portal is directly commited to the GitHub and trigger a rebuild on Netlify. This very post you are reading now is written and published using Netlify CMS admin portal.
The Netlify CMS provides a preview pane which reflects any editing in real-time. However, the default preview pane does not provide some functionalities I need, such as the ability to render math expression and highlight syntax in code blocks. Fortunately, it provides ways to customize the preview pane. The API registerPreviewTemplate can be used to render customized preview templates. One can provide a React component and the API can use it to render the template. This functionality allows me to incorporate markdown-it and prismjs directly into the preview pane.

In this post, I will demonstrate,
- How to write a simple React component for the post.
- How to use markdown-it and prism.js in the template.
- How to pre-compile the template and use it.
A simple React component for custom preview #
I guess a simple preview template would render a title and the body of the markdown text. Using the variable entry provided by Netlify CMS, the template can be written as the following,
// Netlify CMS exposes two React method "createClass" and "h"
import htm from 'https://unpkg.com/htm?module';
const html = htm.bind(h);
var Post = createClass({
render() {
const entry = this.props.entry;
const title = entry.getIn(["data", "title"], null);
const body = entry.getIn(["data", "body"], null);
return html`
<body>
<main>
<article>
<h1>${title}</h1>
<div>${body}</div>
</article>
</main>
</body>
`;
}
});
In the example shown above, I use htm npm module to write JSX like syntax without need of compilation during build time. It is also possible to directly use the method h provided by Netlify CMS (alias for React’s createElement) to write the render template, which is the method given in their official examples.
this.props.entryis exposed by CMS which is a immutable collection containing the collection data which is defined in theconfig.ymlentry.getIn(["data", "title"])andentry.getIn(["data", "body"])access the collection fieldstitleandbody, respectively
Use markdown-it and prism.js in the template #
The problem with the template shown above is that the variable body is just a raw string in markdown syntax which is not processed to be rendered as HTML. Thus, we need a way to parse body and convert it into HTML. To do this, I choose to use markdown-it.
import markdownIt from "markdown-it";
import markdownItKatex from "@iktakahiro/markdown-it-katex";
import Prism from "prismjs";
// customize markdown-it
let options = {
html: true,
typographer: true,
linkify: true,
highlight: function (str, lang) {
var languageString = "language-" + lang;
if (Prism.languages[lang]) {
return '<pre class="language-' + lang + '"><code class="language-' + lang + '">' + Prism.highlight(str, Prism.languages[lang], lang) + '</code></pre>';
} else {
return '<pre class="language-' + lang + '"><code class="language-' + lang + '">' + Prism.util.encode(str) + '</code></pre>';
}
}
};
var customMarkdownIt = new markdownIt(options);
The above codes demonstrate how to import markdown-it as a module and how to configure it.
- I use markdown-it-katex to enable the ability to render math expression.
- I use prism.js to perform the syntax highlighting. Note that the
highlightpart in theoptionsallows the prism.js to add classes to code blocks and used for CSS styling (hence highlighting)
I recommend to use import to load the prism.js module in order to use babel-plugin-prismjs to bundle all the dependencies. I had trouble to get prism.js working in the browser using require instead of import.
Now we have loaded the markdown-it, the body can be translated to HTML using,
const bodyRendered = customMarkdownIt.render(body || '');
To render bodyRendered, we have to use dangerouslySetInnerHTML which is provided by React to parse a raw HTML string into the DOM. Finally, the codes for the template are,
var Post = createClass({
render() {
const entry = this.props.entry;
const title = entry.getIn(["data", "title"], null);
const body = entry.getIn(["data", "body"], null);
const bodyRendered = customMarkdownIt.render(body || '');
return html`
<body>
<main>
<article>
<h1>${title}</h1>
<div dangerouslySetInnerHTML=${{__html: bodyRendered}}></div>
</article>
</main>
</body>
`;
}
});
CMS.registerPreviewTemplate('posts', Post);
Note that there is a new line in the end. There, we use the method registerPreviewTemplate to register the template Post to be used for the CMS collection named posts.
Pre-compile the template #
Now, I have shown how to 1) write a simple template for the preview pane and 2) how to use markdown-it and prism.js in the template. However, the codes shown above cannot be executed in the browser since the browser has no access to the markdown-it and prismjs which live in your local node_modules directory. Here enters rollup.js which essentially can look into the node module markdown-it and prismjs, and take all the necessary codes and bundle them into one big file which contains all the codes needed without any external dependency anymore. In this way, the code can be executed directly inside the browser. To set up rollup.js. We need a config file,
// rollup.config.js
const builtins = require('rollup-plugin-node-builtins');
const commonjs = require('rollup-plugin-commonjs');
const nodeResolve = require('rollup-plugin-node-resolve');
const json = require('rollup-plugin-json');
const babel = require('rollup-plugin-babel');
export default {
input: 'src/admin/preview.js',
output: {
file: 'dist/admin/preview.js',
format: 'esm',
},
plugins: [
nodeResolve({browser:true}),
commonjs({ignore: ["conditional-runtime-dependency"]}),
builtins(),
json(),
babel({
"plugins": [
["prismjs", {
"languages": ["javascript", "css", "markup", "python", "clike"]
}]
]
})
]
};
src/admin/preview.jsis the path of the template code- Set the format to be
esmtells the rollup.js to bundle the code as an ES module. - I use the babel-plugin-prismjs to handle the dependencies of prism.js.
To perform the bundling, one can either use rollup --config in the terminal if rollup.js is installed globally or add it as a npm script. The config above tells the rollup.js to generate the file dist/admin/preview.js.
To use the template, the final step is to include it as a <script type=module> tag. Add the following in the <head> section in your admin/index.html,
<body>
<script type=module src="/admin/preview.js"></script>
</body>
It works! #
See this screenshot
