hapi — Specify a Different Handlebars Layout for a Specific View

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

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.


Additional Resources

Explore the Library

Find interesting tutorials and solutions for your problems.