HTML Template Engines: Pug and Haml Compared with Plain HTML
Compare Pug and Haml with plain HTML: syntax, includes, mixins, escaping, whitespace pitfalls, conversion, and when not to use a template language.

Plain HTML is verbose. Every element needs an opening tag and a closing tag, every attribute needs quotes, and a page of any size ends up as a thicket of angle brackets that hides the structure you are trying to express. Template engines such as Pug and Haml attack that problem from two directions at once: they replace angle brackets with indentation, and they let you mix data, loops and conditions into the markup. This guide is for front-end and full-stack developers who have met a .pug or .haml file in a codebase and want to know what is going on, and for anyone deciding whether to adopt one. You will learn what a template engine actually does, how Pug and Haml differ from each other and from plain HTML, how compile-time and runtime rendering work, where whitespace and escaping bite, how to convert between formats safely, and when the honest answer is "do not use a template language at all".
What is an HTML template engine?
A template engine is a program that takes a template plus some data and produces a finished document. The template contains static markup and placeholders. The data fills the placeholders. The result, for our purposes, is an HTML string that a server sends to a browser or a build step writes to disk.
Two ideas are bundled together in tools like Pug and Haml, and it helps to separate them.
- A different syntax for markup. Instead of
<ul class="products">, you writeul.products. Nesting comes from indentation rather than closing tags. This is purely a notation change. The same tree of elements comes out. - Logic inside the template. Variables, conditionals, loops, includes, layouts and reusable fragments let one template produce many pages. This is what turns a static file into a template.
Plain HTML has neither. It has no variables and no loops, so server-side code or a build step has to generate repeated structure somehow. Template engines exist so that the person writing the page can say "for each product, render a card" right where the card lives.
Indentation-based markup
Indentation-based languages did not appear from nowhere. Python popularised the idea that indentation can carry structure, and designers of markup shorthands noticed that HTML's closing tags are mostly redundant for a human reader: the indentation already tells you where an element ends. If your editor indents nested elements anyway, why type </div> at all?
The tool simply has to do the reverse. This is a minimal sketch in Python that turns an indented outline into nested tags. I ran it, and it printed the output shown below the code.
src = """ul
li A
li B
b bold"""
out, stack = [], []
for line in src.splitlines():
depth = (len(line) - len(line.lstrip())) // 2
tag, _, text = line.strip().partition(" ")
while len(stack) > depth:
out.append(f"</{stack.pop()}>")
out.append(f"<{tag}>{text}")
stack.append(tag)
while stack:
out.append(f"</{stack.pop()}>")
print("".join(out))
Output:
<ul><li>A</li><li>B<b>bold</b></li></ul>
Real engines add attributes, text handling, escaping and error reporting, but the core trick is exactly this: use the depth of each line to decide which elements to close. It also explains why whitespace becomes significant, a theme we return to in the pitfalls section.
A short history
Haml came first. Its authors described it as a markup language that is used to cleanly and simply describe the HTML of a web document without the use of inline code, and it grew up in the Ruby world, especially around Ruby on Rails. The Haml article on Wikipedia summarises its origins and design goals. Haml uses the % character to introduce tag names, . and # for classes and ids, and Ruby for logic.
Pug, which started life under the name Jade, took the same broad idea into the Node.js ecosystem. It is written in JavaScript, it uses JavaScript expressions for logic, and it dropped the % prefix: a line beginning with a word is a tag. The project was renamed from Jade to Pug, a change recorded in the Pug entry on Wikipedia. If you find old tutorials or packages that say Jade, they describe the earlier name of the same family of syntax.
Both sit alongside a much larger family of template languages that keep HTML syntax and add placeholders, such as Mustache, Handlebars, Jinja, Liquid, ERB and EJS. The difference is that those embed logic inside ordinary HTML, while Pug and Haml replace the HTML syntax itself. The rest of this guide uses that distinction to compare them.
I verified the examples in this guide with Pug 3.0.4 on Node.js and Haml 7.5.2 on Ruby 3.3.6. Syntax details can change between major versions, so check the documentation for the version in your project.
The same page in three notations
Seeing one page three ways is the fastest route to understanding what the notation does and does not buy you. Here is a small page in plain HTML.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Product list</title>
</head>
<body>
<header class="site-header" id="top">
<h1>Our products</h1>
</header>
<main>
<ul class="products">
<li class="product" data-id="1">Keyboard</li>
<li class="product" data-id="2">Mouse</li>
</ul>
<p class="note">Prices include tax.</p>
</main>
</body>
</html>
Here is the same page in Pug.
doctype html
html(lang="en")
head
meta(charset="utf-8")
title Product list
body
header#top.site-header
h1 Our products
main
ul.products
li.product(data-id="1") Keyboard
li.product(data-id="2") Mouse
p.note Prices include tax.
I compiled that Pug file with pug.renderFile and the pretty option, and the output matched the HTML above line for line, with one cosmetic difference: Pug writes class before id on the header, as the HTML above also shows. Pug has no closing tags, no angle brackets and no quotes around the class names. The attributes go in parentheses after the tag.
And here is the page in Haml, with the list generated from a Ruby array to show the logic side.
!!! 5
%html{lang: "en"}
%head
%meta{charset: "utf-8"}
%title Product list
%body
%header#top.site-header
%h1 Our products
%main
%ul.products
- products.each do |p|
%li.product{data: {id: p[:id]}}= p[:name]
%p.note Prices include tax.
Haml's % prefix marks tags, attributes go in braces using Ruby hash syntax, a leading - runs Ruby without printing, and = prints the result of a Ruby expression. Rendered with Haml 7.5.2 and two products, the list section came out as <li class="product" data-id="1">Keyboard & Co</li> followed by the second item, with the ampersand escaped automatically.
What the comparison shows
| Aspect | Plain HTML | Pug | Haml |
|---|---|---|---|
| Nesting | Closing tags | Indentation | Indentation |
| Tag syntax | <p> |
p |
%p |
| Class and id | Attributes | p.note#id shorthand |
%p.note#id shorthand |
| Attributes | name="value" |
(name="value") |
{name: "value"} |
| Logic language | None | JavaScript | Ruby |
| Escaping of output | You write entities | Escaped by default | Escaped by default |
| Needs a build or server step | No | Yes | Yes |
| Browser can open the file | Yes | No | No |
The last two rows are the cost side of the ledger. HTML is the only one of the three that every browser understands directly. Pug and Haml are source formats that must be compiled before anything can display them. That compile step is central to every trade-off later in this guide.
How Pug works in technical depth
Pug processes a template in stages: a lexer turns characters into tokens, a parser builds a tree, a linker resolves extends and include, and a code generator produces a JavaScript function. Calling that function with data returns the HTML string. Understanding that pipeline explains most of Pug's behaviour.
Tags, classes, ids and attributes
A line beginning with a word is a tag. A .name adds a class and #name adds an id. If you leave out the tag name, Pug assumes div, so .card becomes <div class="card"></div>. Text after the tag, separated by a space, becomes the element's text.
Attributes go in parentheses, separated by commas or whitespace. Their values are JavaScript expressions. This small file shows several forms, and the output below is what Pug 3.0.4 produced.
const pug = require("pug");
console.log(pug.render('a(href="/x", class="a b", disabled, data-x=1) link'));
<a class="a b" href="/x" disabled="disabled" data-x="1">link</a>
Notice two things. The boolean attribute disabled was written as disabled="disabled", because Pug's default document type renders in XML-compatible form unless you declare doctype html. With doctype html Pug uses the terse HTML5 form for boolean attributes. Also, Pug moved class to the front of the attribute list. Attribute order is not something to rely on in snapshot tests.
Text, inline tags and block text
Pug offers three ways to write text. Text after the tag on the same line is the simplest. A pipe, |, starts a line of plain text inside an element. A trailing dot after a tag, as in p., means the whole indented block is raw text. The dot form is how you write script and style content without Pug trying to parse it as tags:
const pug = require("pug");
console.log(pug.render("script.\n const a = 1;\nstyle.\n p{color:red}"));
<script>const a = 1;</script><style>p{color:red}</style>
Interpolation and unescaped output
Pug has two interpolation styles. #{expression} inserts an escaped value inside text. !{expression} inserts the raw value. For a whole line, = outputs an escaped expression and != outputs an unescaped one. I ran the next example with a hostile-looking string to see the difference.
const pug = require("pug");
const tpl = pug.compile("p= comment\np!= comment");
console.log(tpl({ comment: '<b>hi</b> & "q"' }));
<p><b>hi</b> & "q"</p><p><b>hi</b> & "q"</p>
The first paragraph is safe: the angle brackets, ampersand and quotes became entities. The second paragraph passes the string through verbatim, so the browser sees real HTML. Use the unescaped form only for markup you generated or sanitised yourself. We come back to this in the security section.
Conditionals and iteration
Pug supports if, else if, else and unless, plus case. For loops it offers each and while. A neat feature is each ... else, whose else branch runs when the collection is empty, which saves you an extra if. Here is a combined example that I compiled and ran.
mixin card(p)
article.card(class=p.sale ? 'on-sale' : '')
h2= p.name
p.price $#{p.price.toFixed(2)}
if p.stock > 0
a.btn(href='/buy/' + p.id) Buy
else
span.out Sold out
ul
each p in products
li: +card(p)
else
li No products
Rendered with two products, one in stock and on sale and one out of stock whose name contains angle brackets, Pug produced:
<ul>
<li>
<article class="card on-sale">
<h2>Keyboard</h2>
<p class="price">$49.50</p><a class="btn" href="/buy/1">Buy</a>
</article>
</li>
<li>
<article class="card">
<h2>Mouse <Pro></h2>
<p class="price">$19.00</p><span class="out">Sold out</span>
</article>
</li>
</ul>
The product name Mouse <Pro> was escaped without any effort from the template author. Also notice the li: +card(p) form. The colon is block expansion, a shorthand for putting a child element on the same line as its parent.
The pretty-printer produced an oddity: <a class="btn"> sits on the same line as the <p> before it. Pretty output is a debugging aid, not a guarantee, so do not build anything on its exact layout. Do not diff pretty output and expect stable formatting.
Mixins
A mixin is a reusable function that emits markup. In the example above, mixin card(p) defines it and +card(p) calls it. Mixins accept arguments, can take a block of content from the caller, and can receive extra attributes. They are Pug's closest equivalent to a component, and they work well for repeated fragments inside one project. They are not components in the framework sense: there is no encapsulated state, no lifecycle and no scoped styling.
Includes, extends and blocks
Pug composes pages in two ways. include pastes another file in at that location. extends makes a template a child of a layout, and block marks the regions a child may override. A layout might define a head block and a content block, and a page fills them in.
doctype html
html
head
title #{title} | Shop
block head
body
block content
footer
block footer
p Default footer
A child page extends it:
extends layout
block head
link(rel="stylesheet", href="/app.css")
block content
h1 Hello
include part.pug
Rendered with title set to Home, the result was a complete page with the title Home | Shop, the stylesheet link inside the head, the heading, the included paragraph and the default footer, because the child did not override block footer. The default content inside a block is what makes layouts forgiving.
My first attempt at this example failed, and the error is worth knowing. I had put include part.pug at the top level of the child, outside any block. Pug refused with Only named blocks and mixins can appear at the top level of an extending template. In a template that uses extends, everything must live inside a named block or mixin definition, because the layout decides where content goes. The fix was to indent the include under block content.
Compile time versus runtime
This is the most important operational idea in Pug. Compilation turns a template into a JavaScript function. Rendering calls that function with data. They are different costs and can happen at different times.
- Compile at runtime. Your server calls
pug.renderFileon each request. Pug caches compiled functions when itscacheoption is on, so after the first request the compile cost is mostly gone. This is the simplest setup and the default for Express apps. - Compile at build time. A build step compiles templates to functions or, for fully static pages, renders them to
.htmlfiles with fixed data. Nothing from Pug reaches production except the output. - Compile to client code.
pug.compileClientreturns the source of a JavaScript function that you can ship to a browser. I ran it for a one-line template and the generated source began with a helper calledpug_escape, a reminder that the escaping logic travels with the function.
const pug = require("pug");
const t = pug.compile("p Hello #{name}");
console.log(t({ name: "<script>alert(1)</script>" }));
<p>Hello <script>alert(1)</script></p>
Compile-time work also makes errors earlier: a syntax error in a template is detected when it compiles, not when a user visits the page. Runtime errors, such as reading a property of an undefined value, still appear only when the function executes with real data.
How Haml works in technical depth
Haml's model is similar, but its host language is Ruby, and in a Rails application Haml templates are compiled into Ruby methods alongside ERB templates. The exact integration depends on the gem version, so treat this section as describing the language rather than any one framework setup.
Tags, classes, ids and attributes
A % introduces a tag name. .name and #name add class and id, and with the tag name omitted Haml assumes div, just like Pug. Attributes go in braces as a Ruby hash, or in parentheses in an HTML-like style. Nested hashes under data expand into data- attributes, which is how {data: {id: 1}} became data-id="1" in the example above.
I ran a short template that mixes several of these forms with Haml 7.5.2. The Haml source was:
%a{href: '/x', class: ['a','b'], disabled: true} link
%p
Hello
%b world
and the compiled HTML, as printed by the renderer, was:
<a class="a b" disabled href="/x">link</a>
<p>
Hello
<b>world</b>
</p>
Two points stand out. A boolean attribute with the value true rendered in the terse HTML5 form, disabled, and an array given as the class value was joined with spaces. Also, the nested %b ended up on its own line, with newlines around the text, which is the behaviour whitespace-conscious authors need to remember.
Ruby code and output
A line starting with - runs Ruby without printing anything. A line starting with = evaluates Ruby and prints the result, HTML-escaped. != prints without escaping. Interpolation inside plain text uses #{}, the normal Ruby syntax.
%p= comment
%p!= comment
With comment set to <b>hi</b>, the first paragraph contained <b>hi</b> and the second contained the raw <b>hi</b>. Escaped by default, unescaped on explicit request: the same safety model as Pug, with different punctuation.
Blocks work through Ruby's own constructs. - products.each do |p| opens a loop and the indented lines beneath it are the loop body. You never write end, because indentation closes the block, which is both neat and a source of subtle errors when someone re-indents code carelessly.
Filters
Haml has filters, introduced with a colon. :javascript wraps its content in a script element, :plain passes text through untouched, and :css does the same for styles. Further filters, such as Markdown, depend on which gems are installed. In my test the :javascript filter produced a <script> element with the code indented inside it, and :plain printed raw <b> exactly as typed, with the angle bracket intact. Plain filters do not escape, so treat them as trusted-content-only.
Partials and layouts in Rails
In Rails the idea of partials and layouts is part of the framework, not Haml itself. A partial is a template file whose name starts with an underscore, rendered with render "product" from another view, and Haml partials work exactly like ERB ones. The layout wraps a view and a yield marks where the view goes. Because that machinery belongs to Rails, outside Rails you are on your own to supply layout and partial behaviour. Pug's extends and include are language features, which is a real difference when you use the engine in a plain Node script.
Real-world use cases
Template languages are not a matter of taste alone. Some situations favour them.
- Server-rendered Node apps. Express and similar frameworks can render Pug views directly, with layouts and partials handled by the engine, and the cost per request is just a function call.
- Rails apps with Haml legacy. Many long-lived Rails codebases use Haml throughout. If you maintain one, fluency in Haml is a job skill.
- Static site generation. Pug can render a folder of templates plus JSON data into plain HTML pages during a build, producing files you can host anywhere.
- HTML email. Email markup is a hostile environment of nested tables and inline styles. A mixin or loop that emits the same table row structure removes a large amount of repetition, and the compiled output is ordinary HTML that email clients accept.
- Documentation and prototypes. Quick, tag-light syntax speeds up mockups, and the Pug and Haml conversion tools let you move to plain HTML when a prototype graduates.
- Teams that value consistent indentation. The format forces well-formed nesting. You cannot forget a closing tag, because there is none to forget.
It is worth being honest about which of these have good non-template alternatives. A static site generator with component partials and Markdown content, for example, may serve a documentation site better than hand-written Pug. We compare these options in the alternatives table later.
Common mistakes and pitfalls
Most template headaches are not mysterious bugs in the engine. They are a handful of repeating mistakes.
Mixing tabs and spaces
Both Pug and Haml read indentation as structure. A tab in one line and spaces in another can produce errors or, worse, silently different nesting. Pick spaces, set your editor to insert them, and add an .editorconfig rule so the whole team agrees. Both engines also expect consistent indentation widths within a file.
Wrong indentation changes meaning
In HTML, mis-indenting a tag is a style issue. In Pug and Haml, it is a structural change. If you accidentally indent a line one level deeper, it becomes the child of the line above. Run a quick check by compiling and looking at the output after any bulk re-indent.
Whitespace between inline elements
This is the classic pitfall. Browsers collapse whitespace but not away entirely: a single space between Hello and <b> is meaningful. Pug's compiled output normally contains no whitespace between lines, so this template
p
| Hello
b world
| !
compiled to <p>Hello<b>world</b>!</p>, which renders as Helloworld! with no spaces. To add a space, include one explicitly at the end of the text, or use interpolation with Pug's tag interpolation syntax, #[b world], inside a single line of text. For Pug, the lines p Hello #[b world]! give you what you wanted. The same trap exists in Haml, where you can use whitespace removal markers and helper methods to control spacing. Haml 7.5.2 in my test inserted newlines around nested content, which browsers treat as a single space between inline elements, a different behaviour from Pug's. Both are defensible, and neither is the other's behaviour, so do not assume knowledge transfers.
Unescaped output used on user data
Using != in Haml or !=, !{} in Pug on a value that contains any user-controlled text opens the door to cross-site scripting. The convenience of "I just want the HTML to render" is exactly how injection bugs ship.
Putting business logic in the template
Because the templates can run real code, it is tempting to query databases, format money and call services from them. This turns views into untestable tangles. Keep templates for presentation. Prepare data in a controller or helper, pass in plain values, and keep conditionals simple.
Assuming attribute order
As the earlier example showed, Pug reorders attributes and Haml sorts them. If tests compare rendered strings, compare parsed DOM or use a formatter on both sides, not raw text.
Raw text in script and style blocks
Forgetting the trailing dot on script. makes Pug try to parse your JavaScript as tags and fail. Likewise Haml's :javascript filter is needed for multi-line scripts. If your JavaScript grows beyond a few lines, move it to a real .js file and let the browser cache it.
Including at the wrong level
As the error I hit earlier shows, anything outside a named block in an extending Pug file is a compile error. When you see "Only named blocks and mixins can appear at the top level", look for an include or a tag that sits beside, not inside, a block.
Converting between HTML, Pug and Haml
Teams migrate in both directions: legacy HTML into Pug, or Pug and Haml back out to HTML when they adopt a different stack. Four DevUtilX tools cover the conversions:
- HTML to Pug converts HTML markup into Pug.
- Pug to HTML compiles Pug source into HTML.
- HTML to Haml converts HTML markup into Haml.
- Haml to HTML renders Haml source into HTML.
Our tools run in your browser, and the text you paste is processed locally, so nothing is uploaded to a server. Still, never paste secrets or personal data into any online tool.
What is lost in conversion
The conversions are not symmetrical in what they preserve.
- Direction HTML to Pug or Haml is mostly lossless for static markup. The tree, attributes and text carry across. What you lose is your original formatting: attribute quoting style, blank lines and sometimes comments depending on the tool.
- Direction Pug or Haml to HTML loses everything that makes it a template. Loops are unrolled for the data you supplied, conditionals collapse to one branch, mixins and includes are inlined, and variables become their values. The HTML is correct for that data, but it can no longer produce a different page.
- Browser-side conversions cannot run your real backend code. A converter running in the browser can compile syntax, but Ruby expressions in Haml or server-only helper calls in Pug will not have your application's data or helper methods. Expect to supply sample data, or to convert only the static structure.
- Inline scripts and styles convert fine, but they remain opaque text.
- Conditional comments and unusual doctype strings may need a manual check.
Because the template-to-HTML direction destroys logic, keep your template source under version control and treat the HTML as a build artefact, not the reverse.
Step-by-step: converting with the DevUtilX tools
These steps work in any modern browser and nothing you paste leaves your machine.
Pug to HTML
- Open the Pug to HTML tool.
- Paste your Pug source into the input panel. Start with something static, such as a header and a list, before trying loops.
- Read the generated HTML in the output panel and check the nesting against what you expected.
- If the tool reports an error, look at the line number. Indentation mistakes and unclosed parentheses in attribute lists are the usual causes.
- Copy the HTML into your page or send it to a formatter. For tidy output, our HTML formatting and semantic markup guide explains what readable HTML looks like.
HTML to Pug
- Open the HTML to Pug tool.
- Paste your HTML. Complete documents and fragments both work, but a fragment is a better first test.
- Review the Pug output. Class and id shorthands should appear, and attributes should sit in parentheses.
- Check text that mixes inline tags with words, because spacing there is where converters and humans most often disagree.
- Save the result as a
.pugfile and render it with your engine to make sure the output matches the original.
Haml to HTML and HTML to Haml
The Haml tools work the same way. Open Haml to HTML to see what your Haml renders as, or HTML to Haml to produce Haml from existing markup. When converting Haml that contains Ruby code, do not expect the browser tool to evaluate your application's variables. Replace dynamic parts with sample values first, or convert only the static layout.
A round-trip test
A round trip is a good sanity check. Start with HTML, convert to Pug, convert the Pug back to HTML, and compare with a diff tool. Our guide to comparing files and code diffs explains how to read the differences. Whitespace and attribute order may differ, but the element tree should be identical. If it is not, you have found something worth investigating before you commit a thousand-line conversion.
Code examples you can run
All examples below were run in a sandbox. Where output is shown, it is real output.
JavaScript: render Pug with data
const pug = require("pug");
const template = pug.compile(`
ul
each item in items
li= item
`);
console.log(template({ items: ["alpha", "<beta>", "gamma & delta"] }));
The escaped output, which is exactly what you want when the items come from users, is:
<ul><li>alpha</li><li><beta></li><li>gamma & delta</li></ul>
To produce a static file at build time, call pug.renderFile("page.pug", { pretty: true }) and write the returned string to disk with fs.writeFileSync. I used exactly that call to produce the product page shown at the start.
Ruby: render Haml outside Rails
require "haml"
source = <<~HAML
%ul
- items.each do |item|
%li= item
HAML
context = Object.new
context.define_singleton_method(:items) { ["alpha", "<beta>", "gamma & delta"] }
puts Haml::Template.new { source }.render(context)
This approach, using a plain object as the rendering scope, is how I ran my Haml tests with version 7.5.2. The Haml API has changed across major versions, so if your gem is older, consult its documentation for the correct entry point. Rails hides all of this behind its view layer.
Python: pre-render with a subprocess
Python has no Pug or Haml engine in the standard library. If your build is Python-based and you want to render Pug templates, the honest route is to shell out to Node. This script calls the pug module through node -e and prints the HTML:
import json, subprocess
pug_source = "p Hello #{name}"
data = {"name": "Ada <Lovelace>"}
script = (
"const pug=require('pug');"
"const d=JSON.parse(process.argv[1]);"
"console.log(pug.render(process.argv[2], d));"
)
result = subprocess.run(
["node", "-e", script, json.dumps(data), pug_source],
capture_output=True, text=True, check=True,
)
print(result.stdout.strip())
It needs pug installed in the working directory's node_modules. Passing data through json.dumps and JSON.parse keeps user values out of the command string, which avoids injection into the shell or into the script text. For many Python projects, Jinja2 or a similar native engine is a simpler answer than bridging languages, and we mention that in the alternatives table.
Performance considerations
Template rendering is rarely the slowest part of a web response, but a few facts matter.
- Compiled templates are functions. Once Pug has compiled a template, rendering is a function call that concatenates strings. Cache compiled templates in production so you do not re-parse files on every request. Express does this automatically when the
NODE_ENVsetting isproduction, which sets its view cache option on. - Parsing cost is paid once. The lexing and linking costs show up on startup or on the first request for each template, not on each render.
- Large loops dominate. Rendering ten thousand rows is slow in any engine. Paginate, and do not rely on faster templating to fix an oversized page.
- Client-side compilation ships code.
compileClientoutput includes helper functions. For a handful of templates, that is small, but it is bytes you must send and the browser must parse. - Output size. Pug and Haml emit compact HTML by default, and
prettymode adds whitespace. Do not enable pretty output in production unless you need it. For squeezing HTML further, our guide to HTML minification pros and cons covers what is safe and what is risky.
I measured my own tiny sample to put the size question in proportion. The product page source in Pug was 280 bytes, and the compiled non-pretty HTML was 345 bytes. The Pug source is shorter because it omits closing tags, but this is a sample of one tiny page. The comparison tells you nothing about speed, since the browser receives only the HTML. Source brevity affects the person typing, not the user's connection.
Security considerations
Template engines sit on the boundary between data and markup, which makes them a security-relevant component. Four rules cover most of the risk.
- Escape by default, and keep it that way. Both engines escape
=output and#{}interpolation. I demonstrated that a<script>payload came out as harmless text in the Pug example. Never switch a value to unescaped output just to make a display problem go away. - Treat
!=,!{}and plain-text filters as dangerous. They are legitimate for markup you control, such as the output of a trusted Markdown renderer. If a user can influence the string, sanitise it with a well-maintained HTML sanitiser before it reaches the template. - Escaping is context dependent. HTML entity escaping protects text content and quoted attribute values. It does not make a value safe inside a
scriptelement, inside anhrefthat acceptsjavascript:URLs, or inside an inline event handler orstyleattribute. Validate URLs against an allow-list of schemes, and avoid placing user data in script blocks at all. - Never build templates from user input. Passing user-supplied text as the template source, rather than as data, lets an attacker run the engine's expression language, which in Pug is JavaScript and in Haml is Ruby. This is called server-side template injection. Templates should be files you wrote, and users should only provide data.
The OWASP project publishes extensive guidance on cross-site scripting prevention and is a good next stop for a deeper treatment.
Accessibility considerations
Template languages do not change what the browser sees, so accessibility depends on the HTML they generate. A few points are worth stating anyway.
- Shorthand hides semantics.
.buttonmakes adiv, not abutton. Keystroke-saving defaults such as the implicitdivencourage generic containers when you should usebutton,nav,mainandheader. Write the tag name even when you could skip it. - Attributes are easier to forget when they are tucked in parentheses.
img(src="a.png")with noaltcompiles fine. Make alternative text a habit, or wrap images in a mixin that requires it. - Reusable mixins can enforce accessibility. A single
buttonmixin that always setstypeand takes a label makes the accessible path the easy one. - Check the generated HTML, not the template. Run an accessibility linter against rendered pages, because that is what users and assistive technology receive. Our article on HTML formatting and semantic markup explains landmark and heading structure in more detail.
Editor support, tooling and debugging
Editor support is good but uneven. Syntax highlighting for Pug and Haml is widely available for the mainstream editors, and formatters and linters exist, but they are community maintained and lag behind the tooling for plain HTML. Check that your editor handles indentation guides and auto-indent for these languages, because that is where most of the daily friction comes from.
Debugging practices that pay off:
- Read the error line number. Pug errors include the file name, line and column and print a snippet of the template, as the
part.pug:1:1error earlier showed. - Render small pieces. Compile the fragment in isolation with sample data instead of debugging a whole layout.
- Inspect the HTML. Use your browser's developer tools to see what was produced. The mistake is often a missing space or a misplaced child, and it is visible there at a glance.
- Use
prettylocally only. It makes the structure readable. Compare on parsed DOM rather than on pretty text. - Lint indentation. An editor config that forbids tabs prevents the most common class of error.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Words run together, such as Helloworld |
No whitespace between text and an inline tag | Use #[tag text] interpolation or add an explicit space |
| Compile error about "top level of an extending template" | include or tags outside a named block in a file that uses extends |
Move the content inside block name |
Output contains <b> and you wanted bold |
Escaped output working as designed | Generate the markup in the template, or use unescaped output only for trusted HTML |
Raw <script> appears in a page from user data |
Unescaped output used on untrusted input | Switch back to escaped output and sanitise anything that must carry markup |
Inline script content is parsed as tags |
Missing trailing dot on script. in Pug |
Add the dot, or use the :javascript filter in Haml |
| Element is nested under the wrong parent | Line indented one level too deep | Fix the indentation and recompile |
| Indentation error with no obvious cause | Tabs mixed with spaces | Convert to spaces and enforce with .editorconfig |
undefined appears in the output or rendering throws |
A variable was never passed to the template | Pass the data, or guard with a conditional or default |
| Attributes appear in a different order | The engine reorders attributes | Compare DOM structure, not raw strings |
Boolean attribute renders as disabled="disabled" |
XML-style output without doctype html in Pug |
Declare doctype html at the top |
| Haml tests fail after upgrading the gem | A major version changed the API or rendering | Read the changelog for your version and update the entry point |
Honest trade-offs against modern alternatives
Pug and Haml were a clear win when the main alternative was writing ERB or hand-rolled string concatenation. The landscape has changed, and a fair assessment has to say so.
Advantages that remain:
- Less typing and less visual noise for structure-heavy pages.
- Impossible to produce unbalanced tags.
- Layouts and mixins that cut duplication.
- Good fit for server-rendered apps and static generators.
Disadvantages that matter:
- An extra language to learn and maintain. Every new teammate learns a notation that is not HTML, and every bug report needs translating from the compiled output back to the source.
- A mandatory compile step. You cannot open a Pug file in a browser, paste it into a design tool, or hand it to a designer who knows HTML.
- Thinner tooling. Formatters, linters, accessibility checkers and IDE refactoring assume HTML, JSX or framework templates.
- No component model. Mixins reuse markup but have no scoped state, props validation or lifecycle. Modern interfaces with a lot of client-side interactivity are built with component frameworks or with browser-native custom elements, and Pug or Haml do not replace those.
- Dependency on a particular host. Pug assumes JavaScript on the server or in the build, Haml assumes Ruby. Moving platforms means rewriting or converting every template.
- Shrinking mindshare. Hiring and community support are strongest for the most widely used tools. I have not measured popularity and will not quote numbers, but you should check current usage and maintenance activity for any dependency you adopt.
When not to use a template language
Skip Pug and Haml when:
- The site is mostly static pages with few repeated parts. Plain HTML with a small build step to inject shared headers is enough.
- Your team knows HTML well and is not already invested in Pug or Haml. The conversion cost is real and the benefit is mainly typing speed.
- You are building a highly interactive client application. A component framework or web components will serve you better, and server-side template syntax adds a second system on top.
- Designers or content editors need to edit the markup directly. They will want to work in HTML.
- You need maximum portability of the output. Plain HTML works in every pipeline, email system and CMS with no tooling.
- The project will outlive the toolchain. HTML written in 2005 still renders. A template language needs a working compiler to be readable by a machine at all.
If you decide to remove a template language, the converters above exist for exactly that. Render the templates with representative data, check the output, and replace the logic with your new system's equivalents one view at a time.
Best practices checklist
- Use spaces, never tabs, and configure the editor and
.editorconfigto match. - Write explicit tag names for anything semantic:
button,nav,main,section. - Keep escaping on. Use unescaped output only for strings you generated or sanitised.
- Never pass user-supplied text as template source; pass it as data.
- Keep logic out of templates. Prepare plain values in code and keep conditionals simple.
- Compile once and cache compiled templates in production.
- Do not ship
prettyoutput to production, and do not diff it. - Put shared structure in a layout and repeated fragments in mixins or partials, and give each a clear name.
- Move long scripts and styles into their own files.
- Test the rendered HTML, with a DOM-based comparison and an accessibility check.
- Pin the engine version in your lockfile, because rendering details can change between major versions.
- Keep template source in version control, and treat compiled HTML as an artefact.
- Document why the project uses a template language so future maintainers can judge whether it still earns its place.
Alternatives compared
| Option | Syntax | Logic | Needs compile step | Best for | Weak spot |
|---|---|---|---|---|---|
| Plain HTML | Standard | None | No | Static pages, maximum portability | No reuse without tooling |
| Pug | Indentation | JavaScript | Yes | Node server rendering, static builds | Not HTML, extra tooling |
| Haml | Indentation | Ruby | Yes | Ruby and Rails projects | Ruby dependency, Haml-specific quirks |
| ERB or EJS | HTML with tags | Ruby or JavaScript | Yes | Familiar HTML plus logic | Noisier than indentation syntax |
| Handlebars or Mustache | HTML with placeholders | Minimal by design | Yes | Logic-light templates shared across languages | Limited expressiveness |
| Jinja2 or Liquid | HTML with placeholders | Rich, sandbox-friendly | Yes | Python projects and user-editable templates | Separate dialect to learn |
| Component frameworks (JSX and similar) | HTML-like in code | Full language | Yes | Interactive applications | Heavier toolchain |
| Web components | Standard HTML plus JavaScript | Full language | No | Framework-free reusable widgets | More boilerplate for simple cases |
No row wins everywhere. Choose by the language you already run on the server, the interactivity you need, and who has to edit the files.
FAQ
Is Pug the same as Jade?
Pug is the renamed Jade. The syntax family is the same, though you should check the Pug documentation for current behaviour rather than relying on old Jade tutorials.
Does a browser run Pug or Haml?
No. Both compile to HTML before the browser sees anything. Visitors receive ordinary HTML and never know a template engine was involved.
Is Pug or Haml faster than plain HTML?
Neither is faster for the user. The browser receives HTML in all three cases. A template engine adds a compile and render step on your server or build machine, though that cost is small when compiled templates are cached.
Are Pug and Haml safe against XSS?
They escape output by default, which protects ordinary text. They are not safe if you use unescaped output on untrusted data, if you put user data into script blocks or unsafe URLs, or if you let users supply template source.
Can I use Haml without Rails?
Yes. Haml is a Ruby gem, and I rendered it from a plain Ruby script. You will need to supply the layout and partial behaviour that Rails normally provides.
Can I use Pug in the browser?
You can compile a template to a client-side function with compileClient, but for most projects it is better to render on the server or at build time and send HTML.
Do I lose anything converting HTML to Pug or Haml?
Mostly formatting, such as quote style and blank lines. The element tree and attributes carry across. Converting a template back to HTML loses the logic, because loops and conditionals are resolved for the data you supplied.
Why do my inline tags have no spaces around them?
Whitespace between inline elements is meaningful, and these engines do not add it for you in all cases. Add the space explicitly or use Pug's tag interpolation, #[b text].
Are Pug and Haml still worth learning?
If you work on a codebase that uses them, yes. For a new project, weigh the benefit of terse syntax against the extra build step, thinner tooling and the strength of component frameworks.
Should I minify the HTML that comes out?
Compression on the server does most of the work. Our HTML minification guide explains where minifying helps and where it can break whitespace-sensitive content.
Can DevUtilX tools see my templates?
The conversion tools process your input locally in the browser. As with any tool, avoid pasting secrets or personal data.
Conclusion
Pug and Haml are tidy, opinionated ways to write HTML. They remove closing tags, add loops and layouts, and escape output by default, in exchange for a compile step, a second language and thinner tooling than plain HTML enjoys. If you inherit one, learn its whitespace and escaping rules first, because that is where nearly every bug lives. If you are choosing for a new project, be honest about whether the saving in typing outweighs those costs. When you need to move markup in or out of these formats, try the Pug to HTML, HTML to Pug, Haml to HTML and HTML to Haml converters, all of which run locally in your browser. To keep going, read our guides on HTML formatting and semantic markup and HTML minification pros and cons.
Further reading
Try the tools
Convert your PUG templates into standard HTML markup. This tool is perfect for developers who use the PUG templating engine to write cleaner and more efficient HTML code, improving productivity and code readability.
Transform your standard HTML markup into the clean and efficient PUG templating language. This tool is great for developers looking to refactor their HTML into a more readable format, improving maintainability.
Convert your HAML templates into standard HTML markup. This tool is ideal for developers using the HAML templating engine to create clean and well-structured web pages with ease and efficiency.
Transform your HTML markup into the elegant and concise HAML templating language. This tool is perfect for developers who want to refactor their HTML for better readability and maintainability, improving code quality.


