Render Markdown as HTML in Strapi using controllers
Tags: markdown, node, javascript, webdev
Published: 2021-02-28 22:56:20

[Strapi](https://strapi.io/) is a wonderfully robust headless content management system — one I absolutely adore and believe you will as well — but one shortcoming some users have identified, myself included, is that [there's no built-in option to render HTML](https://github.com/strapi/strapi/issues/2950) from the Markdown generated by the default WYSIWYG text editor.

To address this issue, some users opt to [replace the default editor](https://strapi.io/blog/how-to-change-the-wysiwyg-in-strapi) with one that saves HTML to the database rather than Markdown, such as CKEditor. Others choose to render the Markdown [in their frontend application](https://github.com/nuxt-community/markdownit-module) directly, adding the additional overhead to their site builds, which is not always desirable when working with the JAMstack and every kilobyte in the lambda counts. 

There is, however, another way. While Strapi does not parse your Markdown for you, it does provide a powerful interface for [modifying outgoing API responses](https://strapi.io/documentation/developer-docs/latest/guides/custom-data-response.html), called [controllers](https://strapi.io/documentation/developer-docs/latest/concepts/controllers.html), which allow for you to parse the Markdown in the response itself. 

### It all depends on our dependencies

For this project, I have selected [Marked](https://marked.js.org/) as the Markdown parser we'll be using. Before beginning, install it in your Strapi project's root folder:

```bash
yarn add marked
```

### Head of the class

Create a new folder in your Strapi project's root called `functions`. Fire up your editor and create a new file within the `functions` folder called `md.js`.

We'll now create a new class for our Markdown renderer. The class constructor will take three parameters:

1.  `model`: The model of the collection or single-type whose text fields will be rendered to HTML. From this object, we'll take the value of the `attributes` key.
2.  `types`: The field types to convert to HTML. This will be an object consisting of two arrays: One for `standard` fields to render wrapped in `<p>` tags and one for `inline` fields which will not be wrapped.
3.  `options`: This is an options object with settings corresponding to our chosen Markdown parser.

We'll also specify global defaults and instantiate our Markdown renderer.

```javascript
const defaults = {
  types: {
    standard: ['richtext'],
    inline: ['string']
  },
  options: {
    smartypants: true,
    headerIds: false,
    breaks: true
  }
}

class StrapiMarkdown {
  constructor(
    model,
    types = defaults.types,
    options = defaults.options
  ) {
      this.model = model.attributes
      this.types = types
      
      this.marked = require('marked')
      this.marked.setOptions(options)
    }
}

module.exports = StrapiMarkdown
```

### You're valid

Now that the class is defined, we'll add some validation to ensure that any errors in the data passed to our constructor are caught. We'll do this with some basic `if/else` statements that check whether the necessary fields exist in the passed objects.

For brevity's sake, I've redacted the defaults and module export and will continue in this fashion as we proceed.

```javascript
class StrapiMarkdown {
  constructor(model, types, options) {
    if (model && model.attributes) {
      this.model = model.attributes
    } else {
      throw new Error('`model` must be valid model object')
    }

    if (types && types.standard && Array.isArray(types.standard) && types.inline && Array.isArray(types.inline)) {
      this.types = types
    } else {
      throw new Error('`types` must be object containing `standard` and `inline` arrays')
    }

    if (options && options.constructor === Object) {
      this.marked = require('marked')
      this.marked.setOptions(options)
    } else {
      throw new Error('`options` must be valid object')
    }
  }
}
```

### The method to the madness

Next, we'll add two methods to our class, in the form of arrow functions, so that they inherit the class' `this` object.

The first, `parse`, is the actual data handler. It steps through the model and applies the Markdown parser to the data fields matching those specified in our `types` configuration object. For each key in the collection/single type's model, we'll check if the corresponding key exists in the incoming response object. If so, we'll check whether its type matches either the `standard` or `inline` format, and then apply the Markdown parser.

Because Strapi [passes all responses as promises](https://strapi.io/documentation/developer-docs/latest/concepts/services.html), we'll need to resolve the promise to properly access the data. I prefer the `async/await` syntax, so that's what I'll be using.

```javascript
class StrapiMarkdown {
  constructor(model, types, options) { ... }

  parse = async data => {
    try {
      const item = await data

      for (let key in this.model) {
        if (item[key]) {
          if (this.types.standard.includes(this.model[key].type)) {
            item[key] = this.marked(item[key])
          } else if (this.types.inline.includes(this.model[key].type)) {
            item[key] = this.marked.parseInline(item[key])
          }
        }
      }
      return item
    } catch (err) {
      console.error(err)
    }
  }
}
```

The second method, `md`, determines whether the response data passed in from our controller is an array or a single object and, in turn, passes it to the data handler accordingly. Because we need to resolve all the promises in the `parse` method before passing back the data to our controller, we'll use `Promise.all` to resolve each object as it's mapped over.

```javascript
class StrapiMarkdown {
  constructor(model, types, options) { ... }
  parse = async data => { ... }

  md = data => {
    try {
      if (Array.isArray(data)) {
        return Promise.all(data.map(obj => this.parse(obj)))
      } else {
        return this.parse(data)
      }
    } catch (err) {
      console.error(err)
    }
  }
}
```

### Bringing it all back home

Our completed `md.js` file should now look like this:

```javascript
const defaults = {
  types: {
    standard: ['richtext'],
    inline: ['string']
  },
  options: {
    smartypants: true,
    headerIds: false,
    breaks: true
  }
}

class StrapiMarkdown {
  constructor(
    model,
    types = defaults.types,
    options = defaults.options
  ) {
    if (model && model.attributes) {
      this.model = model.attributes
    } else {
      throw new Error('`model` must be valid model object')
    }

    if (types && types.standard && Array.isArray(types.standard) && types.inline && Array.isArray(types.inline)) {
      this.types = types
    } else {
      throw new Error('`types` must be object containing `standard` and `inline` arrays')
    }

    if (options && options.constructor === Object) {
      this.marked = require('marked')
      this.marked.setOptions(options)
    } else {
      throw new Error('`options` must be valid object')
    }
  }

  parse = async data => {
    try {
      const item = await data

      for (let key in this.model) {
        if (item[key]) {
          if (this.types.standard.includes(this.model[key].type)) {
            item[key] = this.marked(item[key])
          } else if (this.types.inline.includes(this.model[key].type)) {
            item[key] = this.marked.parseInline(item[key])
          }
        }
      }
      return item
    } catch (err) {
      console.error(err)
    }
  }

  md = data => {
    try {
      if (Array.isArray(data)) {
        return Promise.all(data.map(obj => this.parse(obj)))
      } else {
        return this.parse(data)
      }
    } catch (err) {
      console.error(err)
    }
  }
}

module.exports = StrapiMarkdown
```

### Everything is under control

With our class finished, we can now add it to our API's controllers.

Navigate to the `controllers` folder corresponding to the collection or single type whose output you want to modify and open the controller file in your editor (eg. `api/posts/controllers/posts.js`).

First, import the class we created, then the model of the collection or single type.

```javascript
const StrapiMarkdown = require('../../functions/md.js')
const model = require('../models/posts.settings.json')
```

Next, instantiate the class.

```javascript
const { md } = new StrapiMarkdown(model)
```

If you wish to change the fields to be parsed, or adjust the parser options, you can pass those settings in as well.

```javascript
const types = {
  standard: ['richtext', 'text'],
  inline: []
}

const options = {
  smartypants: false,
  headerIds: true,
  breaks: true
}

const { md } = new StrapiMarkdown(model, types, options)
```

Finally, we'll create custom `find` and `findOne` methods to replace the default methods Strapi generates internally. Each method will await the corresponding Strapi service method invoked with its corresponding default parameters but now wrapped in our Markdown class' `md` method.

```javascript
module.exports = {
  async find(ctx) {
    return md(await strapi.services.posts.find(ctx.query))
  },
  async findOne(ctx) {
    const { id } = ctx.params
    return md(await strapi.services.posts.findOne({ id }))
  }
}
```

Once it's all put together you should have:

```javascript
const StrapiMarkdown = require('../../functions/md.js')
const model = require('../models/posts.settings.json')

const { md } = new StrapiMarkdown(model)

module.exports = {
  async find(ctx) {
    return md(await strapi.services.posts.find(ctx.query))
  },
  async findOne(ctx) {
    const { id } = ctx.params
    return md(await strapi.services.posts.findOne({ id }))
  }
}
```

Rinse and repeat for each collection or single type whose output you wish to transform from Markdown to HTML.

### Testing, testing, 1-2-3

Start up your Strapi project and give your API a call!

```bash
curl http://localhost:1337/your_modified_collection
```

If all went well, you should now be seeing HTML instead of Markdown in your API's response data.

### Wrapping up

You should now have some basic grounding in how to create your own custom Strapi controllers to transform your API response data.

If creating the handler seemed like an inordinate amount of work when you can just use something off-the-shelf in your frontend project, bear in mind, you only need to create such a module once and you can then use it over-and-over again in all your different Strapi projects.

In fact, I've already gone ahead and saved you the trouble, turning this tutorial into [an npm module](https://www.npmjs.com/package/strapi-markdown-parser) that you can import into your Strapi controllers so that you never have to roll your own!

```bash
yarn add strapi-markdown-parser
```

Now you can skip every other step and jump straight to:

```javascript
const StrapiMarkdown = require('strapi-markdown-parser')
const model = require('../models/posts.settings.json')

const { md } = new StrapiMarkdown(model)

module.exports = {
  async find(ctx) {
    return md(await strapi.services.posts.find(ctx.query))
  },
  async findOne(ctx) {
    const { id } = ctx.params
    return md(await strapi.services.posts.findOne({ id }))
  }
}
```

Why didn't I just say that in the first place? Well, you wouldn't have learned much then, would you? 😉