Hapi has great built-in support for various template engines like Handlebars or Jade, and HTML itself, of course.
However, the Hapi view documentation suffers from incomplete information when working with Handlebars and only presents rudimentary examples to integrate Handlebars and layouts. Composing a complex layout within a web project with partial views and dynamically inserted content from other files is straight forward when known how to do.
hapi Series Overview
- What You’ll Build
- Prepare Your Project: Stack & Structure
- Environment Variables and Storing Secrets
- Set Up MongoDB and Connect With Mongoose
- Sending Emails in Node.js
- Load the User’s Profile Picture From Gravatar Using Virtuals in Mongoose
- Implement a User Profile Editing Screen
- Generate a Username in Mongoose Middleware
- Displaying Seasons and Episodes for TV Shows with Mongoose Relationship Population
- Implementing Pagination for Movies
- Implement a Watchlist
- Create a Full Text Search with MongoDB
- Create a REST API with JSON Endpoints
- Update Mongoose Models for JSON Responses
- API Pagination for TV Shows
- Customize API Endpoints with Query Parameters
- Always Throw and Handle API Validation Errors
- Advanced API Validation With Custom Errors
- Create an API Documentation with Swagger
- Customize Your Swagger API Documentation URL
- Describe Endpoint Details in Your Swagger API Documentation
- 10 Tips on API Testing With Postman
- JWT Authentication in Swagger API Documentation
- API Versioning with Request Headers
- API Login With Username and Password to Generate a JWT
- JWT Authentication and Private API Endpoints
- Refresh Tokens With JWT Authentication
- Create a JWT Utility
- JWT Refresh Token for Multiple Devices
- Check Refresh Token in Authentication Strategy
- Rate Limit Your Refresh Token API Endpoint
- How to Revoke a JWT
- Invalidate JWTs With Blacklists
- JWT Logout (Part 1/2)
- JWT “Immediate” Logout (Part 2/2)
- A Better Place to Invalidate Tokens
- How to Switch the JWT Signing Algorithm
- Roll Your Own Refresh Token Authentication Scheme
- JWT Claims 101
- Use JWT With Asymmetric Signatures (RS256 & Co.)
- Encrypt the JWT Payload (The Simple Way)
- Increase JWT Security Beyond the Signature
- Unsigned JSON Web Tokens (Unsecured JWS)
- JWK and JWKS Overview
- Provide a JWKS API Endpoint
- Create a JWK from a Shared Secret
- JWT Verification via JWKS API Endpoint
- What is JOSE in JWT
- Encrypt a JWT (the JWE Way)
- Authenticate Encrypted JWTs (JWE)
- Encrypted and Signed JWT (Nested JWT)
- Bringing Back JWT Decoding and Authentication
- Bringing Back JWT Claims in the JWT Payload
- Basic Authentication With Username and Password
- Authentication and Remember Me Using Cookies
- How to Set a Default Authentication Strategy
- Define Multiple Authentication Strategies for a Route
- Restrict User Access With Scopes
- Show „Insufficient Scope“ View for Routes With Restricted Access
- Access Restriction With Dynamic and Advanced Scopes
- hapi - How to Fix „unknown authentication strategy“
- Authenticate with GitHub And Remember the Login
- Authenticate with GitLab And Remember the User
- How to Combine Bell With Another Authentication Strategy
- Custom OAuth Bell Strategy to Connect With any Server
- Redirect to Previous Page After Login
- How to Implement a Complete Sign Up Flow With Email and Password
- How to Implement a Complete Login Flow
- Implement a Password-Reset Flow
- Views in hapi 9 (and above)
- How to Render and Reply Views
- How to Reply and Render Pug Views (Using Pug 2.0)
- How to Create a Dynamic Handlebars Layout Template
- Create and Use Handlebars Partial Views
- Create and Use Custom Handlebars Helpers
- Specify a Different Handlebars Layout for a Specific View
- How to Create Jade-Like Layout Blocks in Handlebars
- Use Vue.js Mustache Tags in Handlebars Templates
- How to Use Multiple Handlebars Layouts
- How to Access and Handle Request Payload
- Access Request Headers
- How to Manage Cookies and HTTP States Across Requests
- Detect and Get the Client IP Address
- How to Upload Files
- Quick Access to Logged In User in Route Handlers
- How to Fix “handler method did not return a value, a promise, or throw an error”
- How to Fix “X must return an error, a takeover response, or a continue signal”
- Query Parameter Validation With Joi
- Path Parameter Validation With Joi
- Request Payload Validation With Joi
- Validate Query and Path Parameters, Payload and Headers All at Once on Your Routes
- Validate Request Headers With Joi
- Reply Custom View for Failed Validations
- Handle Failed Validations and Show Errors Details at Inputs
- How to Fix AssertionError, Cannot validate HEAD or GET requests
Composing the Layout
We'll use the following code structure to separate server code, view configuration and the actual views from each other.
Code Structure
The following block presents the code structure we use for the examples. The views
folder contains any .html
files. Hapi renders the handlebars templates even though they don't have the .hbs
file extension. The layout
folder within views
specifies the default template, used to dynamically include all other files into it.
views/
helpers/
partials/
layout/
default.html
index.html
server.js
The helpers
and partials
folder above can be used for Handlebars helpers and partial layouts. This guide doesn't make use of them and shows them to understand the view configuration options provided by hapi.
Server
The following code can be used to start a basic hapi server. The interesting part is the view configuration, done in lines 12 to 21: server.views({engines: …})
.
var hapi = require('hapi');
// Create hapi server instance
var server = new hapi.Server();
// add connection parameters
server.connection({
host: 'localhost',
port: 3000
});
server.views({
engines: {
html: require('handlebars')
},
path: 'views',
layoutPath: 'views/layout',
layout: 'default',
//helpersPath: 'views/helpers',
//partialsPath: 'views/partials'
});
// create your routes, currently it's just one
var routes = [
{
method: 'GET',
path: '/',
handler: function(request, reply) {
// Render the view with the custom greeting
var data = {
title: 'This is Index!',
message: 'Hello, World. You crazy handlebars layout'
};
return reply.view('index', data);
}
}
];
// tell your server about the defined routes
server.route(routes);
// Start the server
server.start(function() {
// Log to the console the host and port info
console.log('Server started at: ' + server.info.uri);
});
For all hapi view configuration options, have a look at the views documentation or Hapi views tutorial. In this example, we define the path
to our views folder where hapi can find the .html
templates. Additionally, we define a default layout with the layoutPath
configuration and set the file name for our default layout for the layout
option. Since the layout template is called default.html
and located in views/layout/
, hapi will reference the file with the defined values.
Hapi also ships with built-in support for handlebars helpers and partial views. It registers all helpers which are located as .js
files in the path specified within helpersPath
configuration. The same is true for partials.
Default Layout Template
The default layout template is used for every view. Hapi automatically uses this layout and includes the defined subview into it. The most important part within this file is the {{{content}}}
block. This block is used by default to include the contents from every view into this layout file.
views/layout/default.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Your Title</title>
<meta name="HandheldFriendly" content="True" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div class="container">
{{! Every view gets inserted here }}
{{{content}}}
</div>
</body>
</html>
Index View Template
The index view can be seen as a wildcard for any other view. Just define the view of your choice and add a route to it in the server configuration to show it.
The following snippet describes the layout of our index page. Remember: the contents of this file are included into the {{{content}}}
block of default layout.
The first two lines in this file are comments and only add information to this file. It helps for situations when coming back to it after weeks or month of work on other things and you're diving back in.
The handlebars placeholder: {{title}}
and {{message}}
. These two placeholders are used to include the passed data from our handler. Recap the routes and look at the index route. We pass a data
object to the view with corresponding keys: title and message.
views/index.html
{{!< layout/default}}
{{! The tag above means - insert everything in this file into the {content} of the default.html template }}
<h1>{{title}}</h1>
<p>{{message}}</p>
The index template will render like this with provided data:
<h1>This is Index!</h1>
<p>Hello, World. You crazy handlebars layout</p>
Result
The resulting HTML template of the index page will look like this:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Your Title</title>
<meta name="HandheldFriendly" content="True" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<div class="container">
<h1>This is Index!</h1>
<p>Hello, World. You crazy handlebars layout</p>
</div>
</body>
</html>
In case you run into problems using our code snippets, please don't hesitate to contact us via twitter @futurestud_io.