At the time of publishing this blog post, we recently pushed our new startpage and design enhancements for navigation and footer into production. With the release of these design changes, we added more complexity to our layout files. Before, we just had the default layout including navigation, footer and a placeholder for the changing content.
Now, we need to distinct between the startpage and other pages, because the startpage received a hero unit decorated by a purple gradient and a navigation with transparent background. On every other page, the navigation bar uses the purple gradient to provide a consistent layout.
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
The Idea Behind Different Layouts for Specific Views
Natan asked us how to define different layouts for specific views and by the time of his comment, we couldn’t provide a good solution. When working on our design updates for the homepage and blog, we ran into the same issue. We wanted to add a hero unit only on our startpage and keep a consistent layout throughout the rest of the website.
We approach this issue by passing a context
property when rendering the views. We evaluate the context and include either the hero unit including the navigation or only the navigation into our template.
File Structure
We place all template files (ending on .html
) into the views
folder. The default
layout gets its own layout
folder and all other views are placed directly into the views directory or into partials
. Since Handlebars helpers are closely related and used within the view templates, we place them into the helpers
folder inside the views
directory.
The server.js
file contains the required code to bootstrap a basic hapi server to render the views.
The following overview outlines the the file structure:
views/
helpers/
isContext.js
partials/
default-navigation.html
hero-navigation.html
footer.html
layout/
default.html
index.html
server.js
This guide defines a single helper called isContext
to check whether the rendered view corresponds to a given context.
Hapi Server View Configuration
The code below starts a basic hapi server on port 3000
. The view configuration sets a default layout, the path to partial views and helper files as well as the rendering engine.
Further, within the routes
section, we define two routes: the index route available at /
and a second route available at /anotherpage
. The second one is just for illustration purposes to show the idea behind different layouts for specific views.
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',
partialsPath: 'views/partials'
helpersPath: 'views/helpers',
});
// here is the perfect place to add routes :)
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',
context: 'index'
};
return reply.view('index', data);
}
},
{
method: 'GET',
path: '/other-page',
handler: function(request, reply) {
// Render another page than index
var data = {
title: 'Another page',
context: 'anotherpage'
};
return reply.view('anotherpage', 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);
});
Note: keep an eye on the path definition. The example above uses a relative path to the helper folder instead of absolute an path. Starting the node server from another location than the root folder of this example project will result in wrong paths. Node offers options like __dirname
and the path
module to create more robust paths.
Context Helper
The basic idea to render a different layout for specific views with handlebars is to provide a context for your routes and check whether you’re in a given context. We’re going to create and use the isContext
helper to verify the provided context. We define isContext
as a block helper, which has the advantage to provide a fallback option in case the context doesn’t match the desired one.
Let’s look at the code for the isContext
helper:
// Verify the given with desired context
// You need to pass data including the 'context' to the view
var _ = require('lodash')
var isContext
isContext = function(option, viewdata) {
var currentContext = viewdata.data.root.context;
if (!_.isString(currentContext)) {
return viewdata.inverse(this);
}
if (_.isEqual(currentContext, option)) {
return viewdata.fn(this);
}
return viewdata.inverse(this);
};
module.exports = isContext;
Within our server’s view definition, we pass a data object to the view for further evaluation within the helper. We need to extract the currentContext
from the passed data object and verify it’s a string value.
If it’s string value, we check if the given context data is equal to the option value passed while calling the helper. If both values match, the helper returns successfully. If not, it will return to the fallback part.
Combine Helper and Default Layout
That’s the point to assemble the default layout from all the pieces. Our intention is to render a different layout for the startpage and also provide a consistent layout for every other view.
Use the isContext
helper and pass the context name as an option value to the helper. The helper extracts the context value from the server’s view definition and checks whether it equals with the passed option value. In case they do, we include the partial hero-navigation
view.
If the given context and option value don’t match, the block helper falls back into the else
part and renders the included default-navigation
.
<!DOCTYPE html>
<html>
<head>
{{! Document Settings }}
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>{{title}}</title>
</head>
<body>
{{#isContext "index"}}
{{> hero-navigation}}
{{else}}
{{> default-navigation}}
{{/isContext}}
{{! Everything else gets inserted here }}
{{{content}}}
{{> footer}}
</body>
</html>
That’s the solution. We decide whether the context
is index
or not and include the hero-navigation or the default-navigation. Afterwards, the actual view content will replace the {{{content}}}
keyword. Finally, we include the footer’s partial view, because it’s the identical view on every page.