Skip to content
Express.js ex templating 5 min read

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 &lt;admin&gt;</h1>
<p>Welcome back, Ada &lt;admin&gt;!</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.

AspectPugEJS
SyntaxIndentation, no closing tagsLiteral HTML with <% %> tags
Output escaping= / #{} escape by default<%= %> escapes, <%- %> raw
Learning curveHigher — new languageLow — it is HTML plus tags
WhitespaceSignificant (drives structure)Insignificant
Reuseinclude, extends, blockinclude partials

Warning: Because indentation is significant, copy-pasting HTML into a Pug file will not work — you must convert it. Tools like html2pug automate the conversion when migrating an existing template.

Best Practices

  • Set view engine to pug once at startup and render views by name, letting Express resolve the .pug extension.
  • 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 use extends plus block for page layouts instead of repeating boilerplate.
  • Pass a small, view-specific locals object to res.render; rely on app.locals for global values like the site name.
  • Let view cache stay on in production (automatic with NODE_ENV=production) so Pug compiles each template only once.
Last updated June 14, 2026
Was this helpful?