Templating with Pug - Solutions to Common Problems

Today we will learn about Pug, a popular templating engine often used for server-side rendering. Template engines use templates to create HTML, and in Pug these templates can use parameters, inline JavaScript, and other features such as template inheritance. I gained a lot of experience with Pug when I was playing with the monolithic architecture approach to website building, so I figured I might as well share some knowledge on the subject. This will not be a full tutorial, since you can easily find out about the basics using the official Pug documentation.

This blog post will focus more on how to solve some common problems that I encountered. Also, some of these problems may display code from the Express backend framework if it makes defining the problem or solution easier. Without further ado, let us begin.

Passing Arguments from the Backend to the Frontend JavaScript Files

This was one of the first problems I encountered on my Pug journey. If you use Express, then you may know that passing arguments to a pug template is as simple as using the response’s render method when handling a route:

/* GET home page. */
router.get('/', function(req, res, next) {
  const data = { 
    title: 'Express',
    user: 'Davsem'
  }

  res.render('index', { data });
});

In the above example, index refers to an index.pug file in the Express views folder. However, this will not pass the data argument to the frontend JavaScript files. This is harder to do, and after a lot of research, I found that this seems to be the best answer:

 script(nonce=nonce). 
      const backendData = !{JSON.stringify(data)};

You would put this in the head tag of your pug template file. All you have to do now is place all JavaScript files that need to use the backendData variable in the head tag as well. Here is the full pug template that uses this inline script:

doctype html
html
  head
    title= data.title
    link(rel='stylesheet' href='/stylesheets/style.css')
    script(nonce=nonce).
      const backendData = !{JSON.stringify(data)};
    script(defer type='module' src='/javascripts/first.js')
    
  body
    block content

Now there are a couple important things that you should do if you want to use this script:

1 - Make sure you purify all user input.

This is just a general rule for website building. If users can input data, you should purify the data by escaping it. Otherwise, the data could contain malicious scripts which could bring your website to its knees. Never trust users!

2 - Make sure you do not send any sensitive information to the frontend.

You need to keep in mind that any data you send to the frontend can be seen be your users. Make sure to filter out data that is not meant to be seen!

3 - Secure the script with a nonce!

Notice the nonce I gave to the inline script. A nonce is a randomly generated number attached to inline scripts to make them more secure. If a script does not have the nonce, it does not execute. This prevents cross-site scripting attacks, which can easily exploit inline scripts if you happen to not purify some user input. Make sure to generate a new nonce for every request to your website (nonce = ‘number used once’)!

Using Template Inheritance

Template inheritance is a great way to reuse pug code. The pug template I showed before is a layout, meaning it is meant to be inherited by other pug templates. You might have noticed the block at the bottom of the template:

body
    block content

You might know that this means that templates that inherit this template will have to define the ‘content’ block in order to extend the layout. So what if I wanted to place something before or after the block? The first thing that I jumped to was this:

extends layout

include navbar

block content
  h1=data.title
  p Welcome to #{data.title}

include footer

However, this throws an error, since an inheriting template can only have named blocks and mixin definitions at its top/unindented level. How can I add the navbar before my content block and the footer after my content block then? The answer is by using block prepend and append:

extends layout

block content
  h1=data.title
  p Welcome to #{data.title}

block prepend content
  include navbar
  
block append content
  include footer

Another gotcha is trying to use the block append/prepend before you define the block being appended/prepended to. An error is not thrown, but in the following code, neither the block prepend nor the block append are rendered as HTML:

extends layout

block prepend content
  include navbar
  
block append content
  include footer

block content
  h1=data.title
  p Welcome to #{data.title}

Using Methods pug.compile and pug.compileFile

Say you need a way to compile a Pug file without using the Express render method. That is where pug.compile and pug.compileFile come in. Method pug.compile is used when you want to compile a stringified Pug template, and method pug.compileFile compiles a plain Pug template. Consider this pug template, comment.pug:

.comment #{greeting}, my name is #{user}.

Using pug.compileFile to compile this file is simple enough:

const pugPath = path.join(
    process.cwd(),
    'views',
    'comment.pug'
  )
  const template = pug.compileFile(pugPath)
  const renderedHTML = template({
    greeting: 'Hello',
    user: 'Davsem'
  })

This is in the backend within a JavaScript file. I target the pug file that I want to compile, compile it, and then pass the resulting template arguments to complete the HTML. So that’s cool and all, but what’s the issue? Well, if I’m planning to make multiple instances of something, it is better to define a mixin:

mixin commentMixin(greeting, user)
    .comment #{greeting}, my name is #{user}.

This way the template variables are localized to the mixin. Now, say I want to compile the above pug file, commentMixin.pug. The issue is that commentMixin.pug is a Pug file, but not a template; this is because the real template is wrapped within a mixin. As a result, the below code will result in ‘renderedHTML’ containing an empty string:

const pugPath = path.join(
    process.cwd(),
    'views',
    'commentMixin.pug'
  )
  const template = pug.compileFile(pugPath)
  const renderedHTML = template({
    greeting: 'Hello',
    user: 'Davsem'
  })

What do you do here then? You could keep comment.pug for when you want to use pug.compileFile (so comment.pug and commentMixin.pug would both exist), but that is needlessly redundant. Instead, you can use pug.compile. The issue is that a mixin does not add to a pug template unless it is called. Therefore, you need to call the mixin by itself within a template, like so:

const mixinPath = path.join(
    process.cwd(),
    "views",
    "commentMixin.pug",
  )
  const pugString = `include ${mixinPath}\n+commentMixin(greeting, user)`
  const template = pug.compile(pugString, { filename: "comment" })
  const renderedHTML = template({
    greeting: 'Hello',
    user: 'Davsem'
  })

In the above code, pugString is a stringified file. File commentMixin.pug is included on the first line, then there is a line break, and on the second line the mixin is called. The use of filename option in the use of pug.compile is necessary since otherwise an error is thrown. You can read all about the Pug API here

Using the &attributes Feature

Dynamic Mixin Attributes

&attributes is a Pug feature that allows you to add attributes to an HTML tag. The following code adds the ‘baz’ class to a div element:

- const attrs = {}
  - attrs.class = 'baz'
  div(class='biz')&attributes(attrs) I am a div

Note that in the case of the class attribute, it is not overwritten but appended to (the div will have classes biz and baz). Why not simply add the attributes to the HTML tag the normal way? You can do this if you are dealing with a template, but things change when you want dynamic attributes within a mixin. For example, consider this mixin:

mixin iconElement(tag, icon, label=null, attrs={})
    #{tag}.icon-element&attributes(attrs)
        .icon-element__icon
            img(src=icon)
        if label !== null
            .icon-element__label= label

This mixin creates a tag element containing an icon div and an optional label div. The #{tag} is Pug syntax for string interpolation https://pugjs.org/language/interpolation.html; it enables the generalization of the type of the root element returned by the mixin (e.g. button, div, a…).

Now, consider the case where I want to create an iconElement where the root element has classes icon-element and -large, and I want to create another iconElement where the root element has classes icon-element and -small. Without &attributes, there is no way to configure the mixin to allow for both of these mutually exclusive cases without adding a parameter to the mixin expressing whether the class -small or -large should be applied. Instead of adding a parameter for these cases and more parameters for future cases, you can just add a single parameter, attrs, which is an object passed to an &attributes appended to the mixin’s icon-element div:

+iconElement(
    'button',
    '/images/blogPostIcon.png',
    'Blog Posts',
    {
      'class': '-small' 
    }
  )
  +iconElement(
    'button',
    '/images/blogPostIcon.png',
    'Blog Posts',
    {
      'class': '-large' 
    }
  )

This above code creates two buttons of class icon-element, the first given additional class -small and the second given additional class -large.

Mixins within Mixins

You may know that Pug also has a feature called mixin attributes. If this feature was corn, this would be my response:

Jack Black slaps corn out of his face

I spent a lot of time fiddling around with it initially, but I eventually came to the conclusion that there is no need to use it. Basically, it is an &attributes but for a mixin instead of an element. If mixin attributes are appended to a mixin, a keyword variable attributes can be accessed within the mixin to access the attributes. You can actually use it in the iconElement mixin, which makes the attrs parameter useless (which is why it is removed here):

mixin iconElement(tag, icon, label=null)
    #{tag}.icon-element&attributes(attributes)
        .icon-element__icon
            img(src=icon)
        if label !== null
            .icon-element__label= label

Then you can make the two buttons as before using this new mixin like so:

+iconElement(
    'button',
    '/images/blogPostIcon.png',
    'Blog Posts'
  )(class='-small')

  +iconElement(
    'button',
    '/images/blogPostIcon.png',
    'Blog Posts'
  )(class='-large')

Looks great right? That is also what I thought until I started making mixins that depended on other mixins. Consider this mixin, iconElementList:

include iconElement

mixin iconElementList(elementItems)
    ul.icon-element-list&attributes(attributes.iconElementList)
        each item in elementItems
            li.icon-element-list__item
                - item.label = item.label ?? null

                +iconElement(
                    item.tag,
                    item.icon,  
                    item.label
                )(attributes.iconElement)

The goal here is to make a list of icon elements that can be customized via the iconElementList mixin’s mixin attributes. You need to separate attributes affecting the list itself from the attributes affecting the elements, thus the reason for why the attributes variable is expected to contain an iconElementList object and an iconElement object. You can call this mixin like this:

+iconElementList(
    {
      'tag': 'button',
      'icon': '/images/blogPostIcon.png',
      'label': 'Blog Posts'
    },
    {
      'tag': 'button',
      'icon': '/images/blogPostIcon.png',
      'label': 'Blog Posts'
    }
  )(
    iconElementList={
      'class': '-small-gap'
    }
    iconElement={
      'class': '-small'
    }
  )

So the root element of iconElementList will have class -small-gap and each iconElement will have class -small, right? Nope. The -small-gap class is applied, but not the -small class. This is because an object is being passed into where the mixin attributes should be placed for the iconElement:

+iconElement(
    item.tag,
    item.icon,  
    item.label
)(attributes.iconElement)

Pug doesn’t like that, and there is no way to expand the object by using the JavaScript spread notation here. So what can you do? Well, you add an attrs parameter to the iconElement mixin like how it was before we defiled it with mixin attributes. It is at this point that you realize that you will have to add an attrs parameter to each mixin that is called from within another mixin, and so in order to save time, you replace any use of mixin attributes with an attrs parameter. You may also realize that without an attrs parameter, you would not be able to give an iconElement a unique attribute, since attributes.iconElement would be the same for each list item. Anyway, here is the iconElementList mixin without mixin attributes:

mixin iconElementList(elementItems, attrs={})
    ul.icon-element-list&attributes(attrs.iconElementList)
        - let sharedClasses = null
        
        if attrs.iconElement
            - sharedClasses = attrs.iconElement.class 

            if typeof sharedClasses === 'string'
                - sharedClasses = sharedClasses.split(' ')

        each item in elementItems
            li.icon-element-list__item
                - let indepClasses = item.attrs.class

                if typeof indepClasses === 'string'
                    - indepClasses = indepClasses.split(' ')

                if sharedClasses && indepClasses
                    - item.attrs.class = [...sharedClasses, ...indepClasses]
                
                - item.label = item.label ?? null

                +iconElement(
                    item.tag,
                    item.icon,  
                    item.label,
                    {...attrs.iconElement, ...item.attrs}
                )

 And here is an example of calling it:

+iconElementList(
    [{
      'tag': 'button',
      'icon': '/images/blogPostIcon.png',
      'label': 'Blog Posts',
      'attrs': {
        'class': '-small'
      }
    },
    {
      'tag': 'button',
      'icon': '/images/blogPostIcon.png',
      'label': 'Blog Posts',
      'attrs': {
        'class': '-large'
      }
    }],
    {
      'iconElementList': {
        'class': '-small-gap'
      },
      'iconElement': {
        'class': '-colorful'
      }
    }
  )

This way, independent and shared classes can be applied to the icon elements (the first icon element has classes -small and -colorful, and the second icon element has classes -large and -colorful).

Conclusion

In conclusion, solutions to the following common problems have been discussed:

  • Passing arguments from the backend to the frontend JavaScript files
  • Using template inheritance
  • Using methods pug.compile and pug.compileFile
  • Using the &attributes feature

That’s enough Pug for today… now go touch grass or something.

Grass

Comments