Rendering with Pug
Pug — formerly known as Jade — is a whitespace-significant template engine that replaces angle brackets and closing tags with indentation. Instead of writing <h1>...</h1>, you write h1 ... and let nesting describe the document tree. The result is terse, readable markup, and because Pug ships an Express-compatible engine, wiring it into res.render takes only two lines. This page configures Pug, then walks through its syntax for interpolation, iteration, conditionals, and includes.
Installing and configuring Pug
Pug follows the Express engine contract, so no glue code is required — install the package and point the view engine setting at it. Express then resolves res.render("name") to views/name.pug and uses Pug to compile it.
npm install express pug
const express = require("express");
const path = require("path");
const app = express();
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
app.get("/", (req, res) => {
res.render("index", { title: "Home", message: "Hello from Pug" });
});
app.listen(3000, () => console.log("http://localhost:3000"));
A matching views/index.pug describes the page through indentation alone — there are no closing tags, and nesting is the document structure.
doctype html
html(lang="en")
head
title= title
body
h1= message
p Welcome to the site.
Output:
<!DOCTYPE html>
<html lang="en">
<head><title>Home</title></head>
<body>
<h1>Hello from Pug</h1>
<p>Welcome to the site.</p>
</body>
</html>
Indentation, attributes, and classes
A line that begins with a tag name produces that element; whatever you indent beneath it becomes a child. Attributes go in parentheses after the tag, and Pug offers CSS-style shorthand: .foo adds a class, #bar sets an id, and a bare div is implied when you start with a class or id.
a(href="/about" class="btn") About us
img(src="/logo.png" alt="Logo")
.card#main
h2.card-title Pricing
p.card-body Choose a plan that fits.
Output:
<a href="/about" class="btn">About us</a>
<img src="/logo.png" alt="Logo"/>
<div class="card" id="main">
<h2 class="card-title">Pricing</h2>
<p class="card-body">Choose a plan that fits.</p>
</div>
Tip: Indentation must be consistent — pick spaces or tabs for a file and stick with it. Mixing the two, or misaligning a child by even one space, raises a Pug compile error rather than silently producing wrong markup.
Interpolation
Pug distinguishes between buffered output that becomes part of the HTML and unbuffered code that only runs. The = operator after a tag prints an escaped expression, protecting you from XSS. Inside text you can inline a value with #{...} for escaped interpolation or !{...} for raw, unescaped HTML — use the raw form only with trusted content.
res.render("profile", { user: { name: "Ada <admin>" }, bioHtml: "<em>Engineer</em>" });
h1= user.name
p Welcome back, #{user.name}!
p!= bioHtml
Output:
<h1>Ada <admin></h1>
<p>Welcome back, Ada <admin>!</p>
<p><em>Engineer</em></p>
Note how = and #{} escaped the angle brackets in the name, while != left the trusted bio markup intact.
Iteration
Pug has a native each loop for arrays and objects, with an optional index. It reads like a for...of and avoids the closing-tag noise of EJS scriptlets.
res.render("products", {
products: [
{ name: "Keyboard", price: 79 },
{ name: "Mouse", price: 39 },
],
});
ul
each product, i in products
li #{i + 1}. #{product.name} — $#{product.price}
else
li No products available.
Output:
<ul>
<li>1. Keyboard — $79</li>
<li>2. Mouse — $39</li>
</ul>
The optional else block renders when the collection is empty, which removes a common source of empty-list bugs.
Conditionals
Use if, else if, and else for branching. Pug also provides unless (the inverse of if) and a case/when construct that mirrors a switch statement.
if user
p Signed in as #{user.name}
else
a(href="/login") Log in
case user.role
when "admin"
p You have full access.
when "editor"
p You can edit content.
default
p Read-only access.
Includes and reuse
Repeated fragments — headers, footers, navigation — live in their own .pug files and are pulled in with include. The path is relative to the including file, and the extension is optional.
//- views/layout.pug structure
body
include partials/header
block content
include partials/footer
//- views/partials/header.pug
header
nav
a(href="/") Home
a(href="/about") About
For full layouts, combine include with extends and named blocks: a child template declares extends layout and fills the blocks the parent defined, giving you inheritance-based layouts without a separate partials engine.
Pug compared to EJS
Both engines plug into Express the same way; the difference is syntax philosophy. EJS embeds JavaScript inside literal HTML, so designers who know HTML feel at home immediately. Pug replaces HTML with its own indentation-based language — more concise once learned, but a steeper ramp and unforgiving about whitespace.
| Aspect | Pug | EJS |
|---|---|---|
| Syntax | Indentation, no closing tags | Literal HTML with <% %> tags |
| Output escaping | = / #{} escape by default | <%= %> escapes, <%- %> raw |
| Learning curve | Higher — new language | Low — it is HTML plus tags |
| Whitespace | Significant (drives structure) | Insignificant |
| Reuse | include, extends, block | include partials |
Warning: Because indentation is significant, copy-pasting HTML into a Pug file will not work — you must convert it. Tools like
html2pugautomate the conversion when migrating an existing template.
Best Practices
- Set
view enginetopugonce at startup and render views by name, letting Express resolve the.pugextension. - Prefer escaped output (
=,#{}) everywhere and reserve!=/!{}for content you fully trust to avoid XSS. - Keep one indentation style per file — spaces or tabs, never both — since Pug treats misalignment as an error.
- Factor shared markup into partials with
include, and useextendsplusblockfor page layouts instead of repeating boilerplate. - Pass a small, view-specific locals object to
res.render; rely onapp.localsfor global values like the site name. - Let
view cachestay on in production (automatic withNODE_ENV=production) so Pug compiles each template only once.