The Core Problem HTML Preprocessors Solve
Every web developer eventually encounters a moment when writing raw HTML feels unnecessarily repetitive. You find yourself typing the same opening and closing tags, copying header and footer sections between pages, and wishing you could inject dynamic data without string concatenation gymnastics. This is exactly the problem HTML preprocessors and template engines were designed to solve.
The concept isn't new--PHP, which stands for "Hypertext Preprocessor," has been solving this problem since the mid-1990s and powers a significant portion of the web today. But the landscape has evolved dramatically. Modern developers now choose from dozens of specialized template languages, each with distinct philosophies about how HTML should be written and processed.
This guide examines the most popular HTML preprocessors and template engines, comparing their features, syntax approaches, and ideal use cases. Whether you're working with a JavaScript-heavy stack, a Ruby on Rails application, or simply looking to write cleaner, more maintainable HTML, understanding these tools will dramatically improve your development workflow.
According to comprehensive analysis from CSS-Tricks, all HTML preprocessors address two fundamental challenges: composition (breaking HTML into reusable fragments like headers, footers, and navigation) and templating (injecting dynamic data into HTML at render time). CSS-Tricks
Build-Time Versus Runtime Processing
An important distinction shapes your tool choice: when the template gets processed.
Build-time processors like Pug, Haml, and Slim transform source files into HTML during your build process, before any server request. This generates static files that deploy easily and serve quickly. Our web development services team often recommends build-time processing for static site deployments.
Runtime processors like Liquid, EJS, and Nunjucks process templates on-demand when a request arrives, enabling dynamic content but requiring template parsing overhead on each request.
For modern Next.js applications, this distinction often determines whether preprocessed HTML becomes part of your static export or requires server-side rendering. Understanding this helps you choose tools that align with your performance and deployment requirements. Our web development experts can help you select the optimal approach for your specific use case.
Modern HTML preprocessors offer a range of features that go beyond basic templating
Templating
Inject variables and computed values into HTML output using various syntax approaches
Partials/Includes
Break HTML into reusable fragments like headers, footers, and navigation components
Local Variables
Pass scoped data to included templates for more flexible component reuse
Loops
Generate repetitive HTML elements from arrays and data structures
Conditionals
Show or hide content based on data values and boolean logic
Template Inheritance
Create hierarchical layouts with base templates and overrideable blocks
Indentation-Based Preprocessors: Pug, Haml, and Slim
Pug (JavaScript)
Pug, formerly known as Jade, represents the most popular indentation-based preprocessor in the JavaScript ecosystem. Originally inspired by Haml, Pug uses Python-like indentation to define HTML structure, eliminating closing tags entirely, as noted in Bret Cameron's analysis of templating systems. Bret Cameron
1// Pug source2doctype html3html4 head5 title = pageTitle6 script(src='/app.js')7 body8 h1 Hello, #{name}9 .container10 each item in items11 .item= itemThe same markup in standard HTML requires significantly more typing, with every opening tag requiring a corresponding closing tag. Pug's approach reduces character count and forces consistent indentation, which advocates argue produces more readable code.
Pug supports templating through buffered and unbuffered code, includes for partials, mixins for reusable component definitions, and conditional logic with if/else and case statements. Its JavaScript foundation means developers familiar with Node.js find the syntax natural, and npm integration simplifies build tooling. According to the official Pug documentation, the language provides comprehensive support for template inheritance and filters. Pug Documentation
Haml (Ruby)
HAML (HTML Abstraction Markup Language) pioneered the indentation-based approach in the Ruby ecosystem. Like Pug, it uses whitespace to define document structure, but with percent signs (%) preceding tag names rather than bare element names. As documented on CSS-Tricks, HAML remains a popular choice for Rails developers seeking cleaner markup. CSS-Tricks
1// Haml source2%html3 %head4 %title= page_title5 = javascript_include_tag 'application'6 %body7 %h1 Hello, #{name}8 .container9 - items.each do |item|10 .item= itemHAML's Ruby heritage means its iteration syntax (each do...end) mirrors Ruby blocks rather than JavaScript loops. This makes HAML particularly natural for Ruby on Rails developers but potentially unfamiliar to those coming from JavaScript backgrounds. The official Haml documentation emphasizes its focus on beautiful, DRY markup. Haml Documentation
Slim (Ruby)
Slim positions itself as a more minimalist alternative to Haml, using even terser syntax while maintaining similar capabilities. Where Haml uses % for tags and = for output, Slim minimizes punctuation further, as Bret Cameron observed in his comparison of templating systems. Bret Cameron
1// Slim source2html3 head4 title = page_title5 javascript:6 | alert('Hello')7 body8 h1 Hello, #{name}9 .container10 - items.each do |item|11 .itemSlim's pipe character (|) denotes multiline text blocks, while its minimal tag syntax appeals to developers who find even Haml verbose. The trade-off is that Slim's syntax can feel more cryptic to newcomers, though proponents argue this brevity speeds writing once learned. The Slim Lang documentation covers all available features and configuration options. Slim Lang Documentation
| Feature | Pug | Haml | Slim | HTML |
|---|---|---|---|---|
| Tag | div.container | .container | .container | <div class="container"></div> |
| ID | div#main | #main | #main | <div id="main"></div> |
| Text | p Hello | %p Hello | p Hello | <p>Hello</p> |
| Attribute | a(href="/") | %a{href="/"} | a(href="/") | <a href="/"></a> |
| Variable | h1= title | %h1= title | h1 = title | <h1><%= title %></h1> |
| Loop | each i in [1,2] | - @items.each do | - items.each do | <% @items.each do |item| %> |
Modern JavaScript Template Engines
EJS (Embedded JavaScript)
EJS offers a middle ground between full preprocessors and logic-less templates. Its syntax uses standard HTML with <%= %> and <% %> delimiters for JavaScript execution, making it immediately familiar to developers who have worked with PHP or classic ASP. As documented in the comprehensive comparison on Wikipedia, EJS provides a familiar syntax for developers transitioning from server-side templating. Wikipedia
1<!-- EJS source -->2<html>3 <head><title><%= title %></title></head>4 <body>5 <h1>Hello, <%= name %></h1>6 <ul>7 <% items.forEach(function(item) { %>8 <li><%= item %></li>9 <% }); %>10 </ul>11 </body>12</html>EJS processes templates at runtime, evaluating JavaScript expressions within the HTML. This makes it excellent for situations requiring dynamic content but means template parsing happens on every request. For high-traffic applications, the overhead can be significant compared to build-time preprocessors.
Handlebars and Mustache
Mustache describes itself as "logic-less" templates, deliberately omitting conditional and looping syntax from the template itself. All logic lives in the data passed to the template, creating a strict separation of concerns, as documented in the Wikipedia comparison of template engines. Wikipedia
1<!-- Mustache source -->2<html>3 <head><title>{{title}}</title></head>4 <body>5 <h1>Hello, {{name}}</h1>6 <ul>7 {{#items}}8 <li>{{.}}</li>9 {{/items}}10 </ul>11 </body>12</html>The double braces ({{ }}) denote variables, while {{#items}}...{{/items}} sections iterate over arrays. This simplicity means Mustache templates work identically across its 30+ language implementations, making it ideal for applications spanning multiple technology stacks.
Handlebars extends Mustache with additional functionality--helpers, block helpers, and partials--while maintaining compatibility with Mustache templates. This makes Handlebars a common choice when you want logic-less simplicity with escape hatches for more complex requirements.
Nunjucks
Nunjucks, inspired by Jinja2 (Python's template engine), offers extensive functionality with a Pythonic syntax. It provides macros, import capabilities, async support, and a powerful filter system, as documented in the comprehensive template engine comparison. Wikipedia
1<!-- Nunjucks source -->2<html>3 <head><title>{{ title }}</title></head>4 <body>5 <h1>Hello, {{ name }}</h1>6 <ul>7 {% for item in items %}8 <li>{{ item }}</li>9 {% endfor %}10 </ul>11 </body>12</html>Nunjucks processes at runtime and includes features like template inheritance ({% extends %}), blocks ({% block %}), and a rich filter system ({{ name | upper }}). Its comprehensive feature set makes it suitable for complex applications while its familiar syntax lowers the learning curve for those familiar with Python or Jinja2.
Feature-by-Feature Comparison
Templating Capability
All major template engines support injecting variables into output, though the syntax varies. As analyzed by CSS-Tricks, Pug uses buffered expressions (=) and interpolation (#{}), Haml uses = and Ruby interpolation, Slim uses =, EJS uses <%= %>, and curly-brace systems use {{ }} or <%= %>. CSS-Tricks
| Engine | Variable Output | Attribute Values |
|---|---|---|
| Pug | h1= title or h1 #{title} | a(href=url) |
| Haml | %h1= title or %h1 #{title} | %a{href: url} |
| Slim | h1 = title | a(href=url) |
| EJS | <%= title %> | href="<%= url %>" |
| Handlebars | {{title}} | href="{{url}}" |
| Liquid | {{title}} | href="{{url}}" |
Loop Syntax Comparison
Looping and conditional capabilities separate basic templating from powerful template systems. Each engine provides iteration, though the syntax reflects its underlying language.
1// Pug2each item, index in items3 li= index + ': ' + item4 5// Haml6- items.each_with_index do |item, index|7 li #{index}: #{item}8 9// EJS10<% items.forEach((item, index) => { %>11 <li><%= index %>: <%= item %></li>12<% }); %>13 14// Nunjucks15{% for item in items %}16 <li>{{ loop.index }}: {{ item }}</li>17{% endfor %}18 19// Mustache (data-side logic required)20{{#items}}21 <li>{{index}}: {{.}}</li>22{{/items}}Choosing the Right Preprocessor for Your Stack
Modern Web Context
For modern Next.js and React applications, the choice often isn't between preprocessors but between component-based JSX and traditional template engines. React's JSX already provides templating with JavaScript expressions, reducing the need for separate preprocessors in many cases.
However, preprocessors remain valuable for static content sections, email templates, documentation sites, and situations where component overhead feels excessive. Pug integrates well with Express.js servers, while Nunjucks works excellently with static site generators like Eleventy.
Performance Considerations
Build-time processors (Pug, Haml, Slim) generate HTML once, delivering pure HTML files at request time. Runtime processors (EJS, Liquid, Nunjucks) parse templates on every request, adding latency but enabling dynamic content. For static exports and Jamstack deployments, build-time processing typically wins. For server-rendered applications requiring dynamic data, runtime processing's flexibility often justifies the overhead. This decision impacts both SEO performance and user experience.
Recommendation Framework
- Choose Pug if you're in the JavaScript ecosystem, value concise syntax, and can accept indentation sensitivity.
- Choose Haml if you're in the Ruby/Rails ecosystem or prefer percent-tag notation.
- Choose Slim if you want minimal syntax and are comfortable with Ruby integration.
- Choose EJS if you want familiar ASP/PHP-style syntax in JavaScript.
- Choose Handlebars or Mustache if you want cross-platform templates or prefer logic-less separation.
- Choose Nunjucks if you need powerful features with Python-like familiarity.
Best Practices and Common Patterns
Writing Maintainable Templates
Regardless of your chosen preprocessor, certain practices improve maintainability:
- Keep templates focused on presentation, pushing business logic to data preparation layers.
- Use descriptive variable names that document expected content.
- Organize partials in a logical directory structure that mirrors your site's information architecture.
- Document custom filters, mixins, or helpers in README files for team reference.
Debugging Techniques
Template debugging differs from JavaScript debugging. Most preprocessors provide line numbers in error messages, but indentation-related errors can be cryptic. Configuring your editor to highlight indentation and show invisible characters helps. Many systems offer source map support that maps generated HTML back to template lines.
When working with our web development team, we follow these best practices and help clients choose the right tooling for their specific needs and technology stack.