An issue with a lib? Create yours!

I’m working on a Node.js project that is using Koa. I wanted to use Twig as template engine in order to render views. I encountered some issues, and I want to relate how I dealt with.

Many libs and one problem

The usual way to render HTML with Koa is by using koa-views, which is using consolidate under the hood. consolidate require to add and configure manually the template engine we want to use. There are many libs that are dependent (koa-viewsconsolidatetwig). This is not bad, but this requires each lib to communicate correctly with others.

I encountered a problem when I wanted to include or extend other twig files. Let’s take the following code as example.

{# template.twig #}

<!DOCTYPE html>
<html lang="en">
  <head>...</head>
  <body>
    <main>{% block main %}{% endblock %}</main>
  </body>
</html>
{# home.twig #}

{% extends "template.html" %}

{% block main %}
<div>Home</div>
{% endblock %}

This code leads to the following error. Even if add some specific configuration as explained in this issue.

Error parsing twig template undefined:
TwigException: Cannot extend an inline template.

By reading issues, it looked like there was some interferences between these libs, so I decided to reduce their numbers and make the link between Koa and Twig by myself.

The koa-twig lib

The goal of koa-twig is to enhance the Koa Context with a render function. It’s heavily inspired of koa-views.

I’ll present to you the first version of the code. I improved it, and I’m still improving it in order to offer the same feature as the others template engines bindings for Koa (koa-ejs, koa-hbs…).

const twig = require("twig")
const util = require("util")

// Use Promise instead of callback syntax.
const renderFile = util.promisify(twig.renderFile)

/**
 * Give the ability to use Twig template engine in Koa
 * @param {object} config
 * @param {string} config.views - the views folder path
 * @param {object} config.data - the data to pass to each view
 * @param {object} config.extension - the data to pass to each view
 */
const twigMiddleware = (config) => async (ctx, next) => {
  function render(file, data) {
    return renderFile(`${config.views}/${file}.${config.extension || "twig"}`, {
      ...config.data,
      ...data,
    })
  }

  /* `render` function will be accessible
  on ctx and on ctx.response */
  ctx.response.render = render
  ctx.render = render

  await next()
}

module.exports = twigMiddleware

Here a basic use case.

const Koa = require("koa")
const koaTwig = require("koa-twig")

const app = new Koa()

// Configuration of the middleware
app.use(
  koaTwig({
    views: `${__dirname}/views`,
  })
)

app.use(async (ctx) => {
  // Calling `render` will render `./views/home.twig`
  ctx.body = await ctx.render("home")
})

app.listen(8080)

So it wasn’t that hard to reach a quick win. I can now develop with less intermediate. This is also something that made me remember that you don’t need a lib for all of your problems!

Bonus

All the PRs I made for this little lib counted for the Hacktoberfest 🎉

hacktoberfest

Another reason to create and share what you’re doing !

Conclusion

If there is an obstacle on the road and you can’t go to right, try the left. If it’s closed, try to go up. If it’s blocked, dig under! Don’t wait for someone to clean the road for you.

I still have some work to do in order to have the same features as the others Koa template engines bindings, but it’s a first step I’m satisfied of!

The short version : DIY and KISS.

Thanks for reading.

Maxime Blanc


© 2020 Maxime Blanc. Tous droits réservés.