Code blocks and markdown with the Webflow CMS

At Daily.co we use Webflow for our product website and our blog.

We love Webflow. It's a tool with a big sweet spot. It's easy to use, flexible, powerful, and "just works." Webflow has almost everything we could ever want in a web publishing and management tool, and whenever we've run up against the edges of the Webflow feature set it's been easy to add a little bit of custom code.

Recently, we started publishing more blog posts that include code samples, and realized that Webflow doesn't directly support code blocks and syntax highlighting in their Content Management System. So we added that feature! While we were at it, we hacked together markdown rendering support, too. Here's how we did it.

Our blog content lives inside Webflow's Content Management System. The Webflow CMS interface includes a rich text editor. For most of our blog posts, we just copy and paste text right from wherever we've drafted it into the Webflow rich text editor. (Yay, rich text copy and paste standards!)

However, for blog posts that have sample code embedded in them, it's really nice to format that code with syntax highlighting. We tried a few ways to do that using CSS, but didn't come up with styling that was quite what we wanted.

Inspired by a blog post from Mike Ebinum, we realized we could add some javascript to our Webflow CMS template and do complete code syntax highlighting on the client side!

Enter Prism, a syntax highlighting library, and Postscribe, a small library that streamlines DOM rewrites.

Any code block that we want Prism to format, we mark as CODE with a special string on the first line, like this:

-- CODE language-js --
// Here's some code that Prism will highlight
// according to javascript syntax rules. We will
// pass the "language-js" part of the special string
// through to Prism to indicate what syntax rules to
// use. Prism supports lots of languages (see the docs)
// and also some additional special classes, like 
// "line-numbers". So you could do this, too, for example:
// -- CODE language-js line-numbers --

function foo () {}

In our Webflow CMS template we load the Prism and Postscribe libraries. (You can build custom versions of the Prism library with support for only the languages that you need, using this nifty tool.)

<head>
...
  <script src="https://cdnjs.cloudflare.com/ajax/libs/postscribe/2.0.8/postscribe.min.js"></script>
  <script src="https://s3-us-west-2.amazonaws.com/daily-web/prism.js"></script>
  <link href="https://s3-us-west-2.amazonaws.com/daily-web/prism.css" rel="stylesheet" type="text/css" />

  <style>
    /- make prism-formatted code block font a little
       smaller than the default -/
    pre[class-="language-"] {
    font-size: 0.8em;
    line-height: 0.8em;
  }
</head>

And add this javascript down at the bottom of our page body, inside a <script> tag

function formatCode(el) {
    // find our magic declaration string. if we don't find it,
    // do nothing
    let match = el.innerHTML.match(/--\s-CODE\s+(.-)\s---/),
        classNames, codeEl;
    if (match && match[1]) {
      classNames = match[1];
    } else {
      return;
    }
    // strip off the magic string, everything preceding it, and
    // all leading and trailing whitespace
    let txt = el.innerHTML.substring(match.index+match[0].length);
    el.innerHTML = '';
    if (classNames.match(/language-markup/)) {
      codeEl = el.appendChild(document.createElement('script'));
      codeEl.type = 'text/plain';
    } else {
      codeEl = el.appendChild(document.createElement('pre')).
                  appendChild(document.createElement('code'));
    }
    codeEl.className = classNames;
    codeEl.innerHTML = txt;
  }

  let snips = $('p:contains("CODE")');
  snips.toArray().forEach(formatCode);
  Prism.highlightAll();

There are just a couple of subtleties here. First, Webflow loads jquery by default, so we didn't need to explicitly load jquery in our template's <head>. Second, we're using Prism's unescaped markup plugin to format code blocks of type language-markup. The if statement in line 15 determines whether we rewrite the DOM to replace paragraphs with nested <pre><code> tags (as is generally the case for Prism), or with <script> tags (for unescaped markup).

Here's a demo on repl.it of the above code.

But why stop there? We also added in support for inserting github gists. (This was what the sample code from Mike Ebinum that originally inspired us, did, and we figured github gists might be useful in the future.)

Then we realized that often, our draft blog posts start life as markdown-formatted text. And markdown allows for some formatting that the Webflow rich text editor doesn't handle. For example, nested lists. (I'm addicted to using nested lists!)

So we took the same approach — client-side formatting of parts of the DOM — and added markdown support to our blog workflow.

First, in Webflow, we added another text content area to our CMS template.

The idea is that if we are authoring a blog post, we can do it as rich text, or as markdown.

Our markdown field in the Webflow CMS

We use the Showdown markdown converter to turn markdown into HTML. We load it in our document <head> just like we load Prism and Postscribe.

The markdown content area in the Webflow CMS editor outputs HTML. (It's a rich text field, from Webflow's perspective.) So in our client-side code we convert from the RTF/HTML generated by Webflow to bare markdown, and then from there Showdown coverts that markdown back to HTML!

function rtfToMarkdown(el) {
    let txt = el.innerHTML;
    txt = txt.replace(/<p>/g, '');
    txt = txt.replace(/<\/p>/g, '\n\n');
    txt = txt.replace(/<br>/g, '\n');
    txt = txt.replace(/&nbsp;/g, ' ');
    txt = txt.replace(/</g, '<');
    txt = txt.replace(/&gt;/g, '>');
    txt = txt.replace(/&lt;/g, '<');
    // console.log(txt);
    return txt;
  }

  function markdownToHtml(txt) {
    let converter = new showdown.Converter({
      noHeaderId: true,
      headerLevelStart: 2,
      literalMidWordUnderscores: true
    });
    let html = converter.makeHtml(txt);
    // console.log(html);
    return html;
  }

  // if there are elements with a class named 'markdown'
  // attached, first convert their content to html 
  let markdowns = $('.markdown');
  markdowns.toArray().forEach((el) => {
    let txt = rtfToMarkdown(el),
        html = markdownToHtml(txt);
    el.innerHTML = html;
  });

And, of course, we make sure that our code syntax highlighting is still supported in "markdown mode." Here's a repl.it demo showing all the pieces together. Note the nested lists. :-)

It turns out that the markdown support we added to our Webflow CMS template makes it easy for us to collaboratively edit draft posts in Notion. We do more and more of our product planning in Notion, so it's natural for us to draft code-heavy tutorial blog posts in the same tool.

To transfer content from Notion to Webflow, we just "select all" twice in Notion to select all of the blocks of content on a page. Then in Webflow, we "paste as plain text" into the markdown content block. That's it! So easy.

The only things that don't work automagically are images. We still have to add images into the Webflow post by hand. But when Notion launches an API, we'll probably be able to automate those, too!

We hope this has been a useful show-and-tell about how we extended Webflow's CMS for our workflows and particular blogging needs. If anything's not clear, or you have any suggestions for how to improve on what we've done, please tweet to us at @trydaily.

Never miss a story

Get the latest direct to your inbox.