Customize Netlify CMS preview with Markdown-it and Prism.js

Aug 26, 2019

This site is hosted on Netlify and con­fig­ured with Netlify CMS. I nor­mally would like to write my post or other con­tents on this site us­ing vim or other text ed­i­tors. However, some­times it is con­ve­nient to be able to edit con­tents on­line (in the browser) and a CMS al­lows me to do just that. I can just lo­gin https://​www.shis­guang.com/​ad­min/ in any com­puter and start edit­ing. In ad­di­tion, a CMS pro­vides UI to eas­ier edit­ing. The post writ­ten and saved in the ad­min por­tal is di­rectly com­mited to the GitHub and trig­ger a re­build on Netlify. This very post you are read­ing now is writ­ten and pub­lished us­ing Netlify CMS ad­min por­tal.

The Netlify CMS pro­vides a pre­view pane which re­flects any edit­ing in real-time. However, the de­fault pre­view pane does not pro­vide some func­tion­al­i­ties I need, such as the abil­ity to ren­der math ex­pres­sion and high­light syn­tax in code blocks. Fortunately, it pro­vides ways to cus­tomize the pre­view pane. The API registerPreviewTemplate can be used to ren­der cus­tomized pre­view tem­plates. One can pro­vide a React com­po­nent and the API can use it to ren­der the tem­plate. This func­tion­al­ity al­lows me to in­cor­po­rate mark­down-it and pris­mjs di­rectly into the pre­view pane.

Editing in the Netlify CMS ad­min por­tal. The right hand side is the pre­view pane

In this post, I will demon­strate,

  • How to write a sim­ple React com­po­nent for the post.
  • How to use mark­down-it and prism.js in the tem­plate.
  • How to pre-com­pile the tem­plate and use it.

A sim­ple React com­po­nent for cus­tom pre­view #

I guess a sim­ple pre­view tem­plate would ren­der a ti­tle and the body of the mark­down text. Using the vari­able entry pro­vided by Netlify CMS, the tem­plate can be writ­ten as the fol­low­ing,

// 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 ex­am­ple shown above, I use htm npm mod­ule to write JSX like syn­tax with­out need of com­pi­la­tion dur­ing build time. It is also pos­si­ble to di­rectly use the method h pro­vided by Netlify CMS (alias for React’s createElement) to write the ren­der tem­plate, which is the method given in their of­fi­cial ex­am­ples.

  • this.props.entry is ex­posed by CMS which is a im­mutable col­lec­tion con­tain­ing the col­lec­tion data which is de­fined in the config.yml
  • entry.getIn(["data", "title"]) and entry.getIn(["data", "body"]) ac­cess the col­lec­tion fields title and body, re­spec­tively

Use mark­down-it and prism.js in the tem­plate #

The prob­lem with the tem­plate shown above is that the vari­able body is just a raw string in mark­down syn­tax which is not processed to be ren­dered as HTML. Thus, we need a way to parse body and con­vert it into HTML. To do this, I choose to use mark­down-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 demon­strate how to import mark­down-it as a mod­ule and how to con­fig­ure it.

  • I use mark­down-it-ka­tex to en­able the abil­ity to ren­der math ex­pres­sion.
  • I use prism.js to per­form the syn­tax high­light­ing. Note that the highlight part in the options al­lows the prism.js to add classes to code blocks and used for CSS styling (hence high­light­ing)

I rec­om­mend to use import to load the prism.js mod­ule in or­der to use ba­bel-plu­gin-pris­mjs to bun­dle all the de­pen­den­cies. I had trou­ble to get prism.js work­ing in the browser us­ing require in­stead of import.

Now we have loaded the mark­down-it, the body can be trans­lated to HTML us­ing,

const bodyRendered = customMarkdownIt.render(body || '');

To ren­der bodyRendered, we have to use dangerouslySetInnerHTML which is pro­vided by React to parse a raw HTML string into the DOM. Finally, the codes for the tem­plate 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 reg­is­ter the tem­plate Post to be used for the CMS col­lec­tion named posts.

Pre-compile the tem­plate #

Now, I have shown how to 1) write a sim­ple tem­plate for the pre­view pane and 2) how to use mark­down-it and prism.js in the tem­plate. However, the codes shown above can­not be ex­e­cuted in the browser since the browser has no ac­cess to the mark­down-it and pris­mjs which live in your lo­cal node_modules di­rec­tory. Here en­ters rollup.js which es­sen­tially can look into the node mod­ule markdown-it and prismjs, and take all the nec­es­sary codes and bun­dle them into one big file which con­tains all the codes needed with­out any ex­ter­nal de­pen­dency any­more. In this way, the code can be ex­e­cuted di­rectly in­side the browser. To set up rollup.js. We need a con­fig 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.js is the path of the tem­plate code
  • Set the for­mat to be esm tells the rollup.js to bun­dle the code as an ES mod­ule.
  • I use the ba­bel-plu­gin-pris­mjs to han­dle the de­pen­den­cies of prism.js.

To per­form the bundling, one can ei­ther use rollup --config in the ter­mi­nal if rollup.js is in­stalled glob­ally or add it as a npm script. The con­fig above tells the rollup.js to gen­er­ate the file dist/admin/preview.js.

To use the tem­plate, the fi­nal step is to in­clude it as a <script type=module> tag. Add the fol­low­ing in the <head> sec­tion in your admin/index.html,

<body>
  <script type=module src="/admin/preview.js"></script>
</body>

It works! #

See this screen­shot

Wondering what that equa­tion means? Checkout Crooks Fluctuation Theorem!