Template Converter

On this page, you’ll learn how to use templates to have full control over the output.

Built-in template engines

By default, Asciidoctor.js supports the following template engines with the corresponding file extensions:




.handlebars, .hbs


.nunjucks, .njk



Please note that the dependencies are optional, so you will need to install them explicitly. For instance, if you want to use Nunjucks, you will need to install the nunjucks package:

$ npm i nunjucks

Once the dependency is installed, you can create template files in a directory.

Plain JavaScript templates

Asciidoctor.js also supports templates written in plain JavaScript. In this case, you should write JavaScript files that export a default function:

module.exports = ({ node }) => `<p class="paragraph">${node.getContent()}</p>`

This function will be called with a Template context as argument.

Naming convention

Let’s say, we want to use Nunjucks to write templates. We create a directory named templates and a file named paragraph.njk:

<p class="paragraph">{{ node.getContent() | safe }}</p>

By default, Nunjucks will automatically escape all output for safety. Here, we are using the built-in safe filter to mark the output as safe. As a result, Nunjucks will not escape this output.

As mentioned above, the file extension njk is important because it tells Asciidoctor.js that this file is a Nunjucks template. Moreover, the name paragraph is also important as it matches a node name. For reference, here’s the complete list of node’s name:

  • document

  • embedded

  • outline

  • section

  • admonition

  • audio

  • colist

  • dlist

  • example

  • floating-title

  • image

  • listing

  • literal

  • stem

  • olist

  • open

  • page_break

  • paragraph

  • preamble

  • quote

  • thematic_break

  • sidebar

  • table

  • toc

  • ulist

  • verse

  • video

  • inline_anchor

  • inline_break

  • inline_button

  • inline_callout

  • inline_footnote

  • inline_image

  • inline_indexterm

  • inline_kbd

  • inline_menu

  • inline_quoted

You don’t need to create a template for all the nodes. Asciidoctor.js can fallback on a built-in converter. For instance, we can use the built-in HTML 5 converter for every node except for paragraph nodes where we use a custom template.

Templates directory

You can instruct Asciidoctor.js to use a template directory from the CLI with the --template-dir option (or -T for short):

$ asciidoctor --template-dir ./templates doc.adoc

You can also configure the template directory using the API:

asciidoctor.convertFile('doc.adoc', { safe: 'safe', backend: 'html5', template_dir: './templates' })

Multiple templates directories

It’s also possible to use more than one template directory. In this case, we can repeat the --template-dir option from the CLI:

$ asciidoctor --template-dir ./templates-a --template-dir ./templates-b doc.adoc

In the above command, we are using two templates directories named templates-a and templates-b.

From the API, we will need to define the template_dirs option:

asciidoctor.convertFile('doc.adoc', { safe: 'safe', backend: 'html5', template_dirs: ['./templates-a', './templates-b'] })

Conflicts resolution

Here’s how Asciidoctor.js resolve conflicts in the following situations:

Two or more templates are defined for the same node name in the same directory

For instance, you have a paragraph.njk and a paragraph.hbs file in your template directory. In this case, the rule is "last one wins" in alphabetical order. Since njk is after hbs in alphabetical order, Asciidoctor.js will use the Nunjucks template over the Handlebars template.

Two or more templates are defined for the same node in different directories

For instance, we have a paragraph.njk in template-a directory and a paragraph.njk in template-b directory. In this case, the rule is still "last one wins" but the order in the template_dirs option is important.
If I declare the following:

const options = { template_dirs: ['template-a', 'template-b'] }

Then template-b/paragraph.njk will win because it’s effectively the last one. Now, if I change the order in the template_dirs option:

const options = { template_dirs: ['template-b', 'template-a'] }

Then template-a/paragraph.njk will win!

Please note that it’s not a recommended practice, and you should try to avoid conflicts upstream.

helpers.js file

You can create a helpers.js file in your template directory. This file can be used to declare utility functions that can be used in the templates. For instance, if you are using Handlebars, you might want to register partials or helpers. Similarly, if you are using Nunjucks, you might want to add custom filters.

If this file exists, Asciidoctor.js will load it (using the Node.js require directive) and call the configure function if it’s exported:

module.exports.configure = (context) => {
  // ...

The context object will contain an isolated environement (if supported) template engine keyed by template engine name:


An isolated Handlebars environment obtained via Handlebars.create()


An isolated Nunjucks environment obtained via nunjucks.configure()

Here’s a concrete example where we add a Nunjucks filter shorten which returns the first count characters in a string, with count defaulting to 5:

module.exports.configure = (context) => {
  context.nunjucks.environment.addFilter('shorten', (str, count) => str.slice(0, count || 5))

Isolated environment

An isolated environment means that each environment has its own helpers, partials, filters…​ It’s worth noting that an environment is isolated per template directory.
For instance, if we define a value with the same name in two directories the last one won’t overwrite the first one:

module.exports.configure = (context) => {
  context.nunjucks.environment.addGlobal('cdn', '//cdn.web.com')
module.exports.configure = (context) => {
  context.nunjucks.environment.addGlobal('cdn', '//cdn.blog.io')

With the above definition, the value cdn will be equals to:


if we are using the template directory web


if we are using the template directory blog


EJS, Plain JavaScript and Pug templates do not rely on an "environment".
As a result, you don’t need to define a configure function. Instead, you can use the helpers.js file to export values and functions that will be accessible in all templates:

let assetUriScheme
module.exports.version = '1.0.0'
module.exports.getAssetUriScheme = (document) => {
  if (assetUriScheme) {
    return assetUriScheme
  const scheme = document.getAttribute('asset-uri-scheme', 'https')
  assetUriScheme = (scheme && scheme.trim() !== '') ? `${scheme}:` : ''
  return assetUriScheme

In the above example, the value version and the function getAssetUriScheme will be available on the helpers key in the template context:

module.exports = function ({ node, _, helpers }) {
  const target = node.getAttribute('target')
  const document = node.getDocument()
  const src = `${helpers.getAssetUriScheme(document)}//www.youtube.com/embed/${target}}` (1)
  return `<figure class="video"><iframe src="${src}" frameborder="0"/></figure>`
1 Use the getAssetUriScheme function defined in the helpers.js file

Template context

Asciidoctor.js will pass the following context to the template:


An AbstractNode from the Asciidoctor.js AST. Depending on the context, it can be a Section, a Document, a Block…​
We recommend reading the JS API documentation to find out what it’s available on each Node.


An optional JSON of options.


The functions and values exported from the helpers.js file.

Template options

You can configure the template engine using the template_engine_options option. Here’s a few examples:

const options = {
  template_engine_options: {
    nunjucks: {
      autoescape: false
    handlebars: {
      noEscape: true
    pug: {
      doctype: 'xml'
    ejs: {
      delimiter: '?',
      openDelimiter: '[',
      closeDelimiter: ']'

To find out which options you can use, please read the official documentation of your template engine:

Template cache

For performance reasons, templates are cached, but you can disable this feature using the template_cache option:

asciidoctor.convert(input, { template_cache: false })

It might be useful when you want to configure the same template directory with different options. In the following example, we want to an XML doctype. We need to disable the cache otherwise the second conversion will not reconfigure the templates with the doctype: 'xml' option:

const options = {
  safe: 'safe',
  doctype: 'inline',
  backend: 'html5',
  template_dir: '/path/to/templates/pug',
  template_cache: false, // disable template cache

console.log(asciidoctor.convert(`image:cat.png[]`, options)) // <img src="cat.png"/>
console.log(asciidoctor.convert('image:cat.png[]', Object.assign(options, {
  template_engine_options: {
    pug: {
      doctype: 'xml'
}))) // <img src="cat.png"></img>