# How Handlebars (hbs) Template Rendering Works in the Website-Downloader App

> Learn how Handlebars (hbs) template rendering works in Website-Downloader. See how it compiles .hbs files into HTML by injecting context data and using layout.hbs with {{{body}}}.

- Repository: [Ahmed Ibrahim/Website-downloader](https://github.com/AhmadIbrahiim/Website-downloader)
- Tags: internals
- Published: 2026-07-08

---

**Handlebars template rendering in this Express application compiles `.hbs` files into HTML by injecting route-level context data into templates and automatically wrapping the output inside `views/layout.hbs` using the `{{{body}}}` placeholder.**

The Website-downloader repository leverages Express with the `hbs` view engine to generate dynamic HTML pages for its website archiving interface. Understanding how Handlebars template rendering integrates with Express routing enables developers to customize existing views or extend the application with new pages. This article examines the complete rendering pipeline from engine configuration to the final HTTP response.

## Configuring the hbs View Engine in app.js

The rendering chain begins in [`app.js`](https://github.com/AhmadIbrahiim/Website-downloader/blob/main/app.js) where the application registers Handlebars as the template engine and defines the views directory. According to the source code, two critical configuration calls establish this setup:

```javascript
app.set('view engine', 'hbs');
app.set('views', path.join(__dirname, 'views'));

```

The first line tells Express to use the `hbs` engine for any file extension matching the registered view engine. The second line explicitly sets the views directory to `./views`, ensuring Express knows where to locate template files when `res.render()` is invoked.

## The Five-Step Template Rendering Pipeline

When a user visits a route that returns HTML, the following pipeline executes:

### 1. Route Handler Invokes res.render()

In [`routes/index.js`](https://github.com/AhmadIbrahiim/Website-downloader/blob/main/routes/index.js), the home route triggers the rendering process by calling `res.render()` with a view name and context object:

```javascript
router.get('/', (req, res) => {
  res.render('index', { 
    title: 'Website downloader – Take any website offline.' 
  });
});

```

The string `'index'` maps to `views/index.hbs`, while the object provides the template context containing the `title` variable.

### 2. Handlebars Compiles the View Template

The `hbs` module reads `views/index.hbs` and parses Handlebars expressions such as `{{title}}`. It creates a rendering function that substitutes these placeholders with matching properties from the context object passed by the route.

### 3. Layout Wrapping via {{{body}}}

The `hbs` engine automatically loads `views/layout.hbs` as the default wrapper. Inside this layout file, line 16 contains the placeholder `{{{body}}}` (triple curly braces to prevent HTML escaping), which is replaced with the compiled output of `index.hbs`. This produces a complete HTML document structure:

```hbs
<!DOCTYPE html>
<html>
  <head>
    <title>{{title}}</title>
  </head>
  <body>
    {{{body}}}
  </body>
</html>

```

### 4. Final HTML Assembly

The engine combines the layout skeleton with the view content, injecting the `title` variable into both the layout's `<title>` tag and the view's markup where referenced.

### 5. Express Sends the Response

The fully assembled HTML string is written to the HTTP response and sent to the client, completing the rendering cycle.

## Template Structure and Context Injection

The repository separates layout concerns from page-specific content using two distinct template types:

- **`views/layout.hbs`** – Provides the global page structure, loads CSS/JS assets, and defines the `{{{body}}}` insertion point.
- **`views/index.hbs`** – Contains the home page markup, receiving the `title` variable from the route handler to render dynamic content.

Any property passed in the `res.render()` context object becomes available in the template via `{{propertyName}}` syntax.

## Practical Example: Adding a New Page

To extend the application with an About page, create both a route handler and a corresponding template:

**File: [`routes/about.js`](https://github.com/AhmadIbrahiim/Website-downloader/blob/main/routes/about.js)**

```javascript
var express = require('express');
var router = express.Router();

router.get('/about', (req, res) => {
  res.render('about', {
    title: 'About – Website Downloader',
    description: 'A tiny tool to fetch and archive any public website.'
  });
});

module.exports = router;

```

**File: `views/about.hbs`**

```hbs
<h2>{{title}}</h2>
<p>{{description}}</p>

```

When a user visits `/about`, Express renders `about.hbs` with the supplied context, wraps it in `layout.hbs`, and returns the complete page.

## Summary

- **Engine registration** occurs in [`app.js`](https://github.com/AhmadIbrahiim/Website-downloader/blob/main/app.js) via `app.set('view engine', 'hbs')`, establishing Handlebars as the default template processor.
- **Route-level rendering** uses `res.render(viewName, context)` in files like [`routes/index.js`](https://github.com/AhmadIbrahiim/Website-downloader/blob/main/routes/index.js) to specify which template to process and what data to inject.
- **Automatic layout wrapping** inserts view content into `views/layout.hbs` at the `{{{body}}}` placeholder, ensuring consistent page structure.
- **Context variables** passed to `res.render()` become available as `{{variable}}` expressions within both the view and layout templates.

## Frequently Asked Questions

### What is the difference between {{body}} and {{{body}}} in Handlebars?

Double curly braces `{{body}}` escape HTML content to prevent XSS attacks, rendering tags as plain text. Triple curly braces `{{{body}}}` output raw HTML unescaped, which is essential in `views/layout.hbs` line 16 to ensure the rendered view markup is interpreted as HTML rather than text.

### How do I pass data from an Express route to a Handlebars template?

Pass a context object as the second argument to `res.render()`. For example, `res.render('index', { title: 'My Page' })` makes `title` available in the template as `{{title}}`. Nested objects are accessible via dot notation: `{{user.name}}`.

### Can I use a different layout than layout.hbs?

Yes, though the repository uses the default `views/layout.hbs`. You can specify an alternative layout by passing a layout property in the context: `res.render('index', { layout: 'alternate', title: 'Home' })`, which would use `views/alternate.hbs` instead.

### Where does Express look for hbs templates by default?

According to the [`app.js`](https://github.com/AhmadIbrahiim/Website-downloader/blob/main/app.js) configuration, Express looks in the `views` directory defined by `app.set('views', path.join(__dirname, 'views'))`. When you call `res.render('index')`, it resolves to `views/index.hbs` relative to the application root.