Serving Dynamic Web Pages Using HapiJS

October 11, 2017
dynamic html hapijs howto nodejs

Introduction

In this post, I will explain how to serve a web page using HapiJS with some dynamic content features. I’ll be focusing mostly on the technical part (like most of the posts I’ll publish here on my blog), however, and only for personal reasons, I will contextualize this scenario and tell you why I followed this solution, but it will be just a brief summary in case you want to know this little “adventure” of mine. If you are only interested in the technical solution please click here.

It’s been a long time since I wanted to have my own website to post some technical tutorials and list a few projects I have in my “bucket”. Before I created this website you are reading right now (using Hugo), I’ve tried several solutions from creating pure static HTML pages to using and customizing a whole new Wordpress theme. I mostly ended giving up on them either because the complexity of my solution was heading into something I wasn’t satisfied with, or because the maintenance was getting higher and more confusing according to the contents of the website itself.

The last solution I tried (before this actual one) was using a NodeJS server application to serve some web pages through HapiJS and customizing a few layouts dynamically using a feature called helpers. That way I only needed to focus on creating my contents inside text files and making my application read them and generate on the generic layout (once that one is first created), and everything was processed only when requested.

The main reason I gave up on this solution was only that I had to style all the generic pages from scratch. If I had a good template for that already, I could have finished that part quickly and focused on writing some content for my website. But after all, I still think it’s a good option in case you want something fast (performance), relatively simple, and totally customizable.

In the following chapters, I will explain in detail how to create this dynamic content using HapiJS. So let’s go to the solution already!

Contents

1. Installation

This tutorial assumes you already have NPM and NodeJS installed. If you don’t have them you can go to their website and follow the installation instructions.

NodeJS website | NPM website

Now, on a terminal command and inside your working directory, initiate npm by typing npm init -y to create a package file and start your project. (the option -y means “yes to all questions”)

> npm init -y

We’re using HapiJS to create the server in our project. Basically, HapiJS is a framework that makes your life easier when building applications and services. You can also check their website here for more information and the full documentation. Now that we have NPM, installing the necessary components is as easy as typing npm install <what> --save.

> npm install hapi --save

We’ll use a HapiJS plugin called Inert to host static files such as images, css styles, scripts, etc.

> npm install inert --save

Another plugin needed for this project is the Vision to support and render templates.

> npm install vision --save

Finally, we’ll make use of the Handlebars engine to write some dynamic contents.

> npm install handlebars --save

2. Hands On (Coding)

So, let’s start building the structure of our application for organization purposes.

Create a directory called data to store the raw information of the website.

Create a directory called routes to store the routes of the website (modularization).

Create a directory called website to store templates and static files of the website (we’ll explain this directory later).

Create a file called index.js in the root of the working directory and this will be the main function to be called.

Inside the index.js file let’s declare the framework, plugins, and engine we installed before. We also need to set up the host name and port of our server, which in this case will be localhost:3000.

"use strict"
const Hapi = require('hapi')
const server = new Hapi.Server()
const Vision = require('vision')
const Handlebars = require('handlebars')
const Inert = require('inert')

const routesPath = './routes/'

// connection configurations
server.connection({
    port: 3000,
    host: 'localhost'
})

2.1. Registering The Plugins

To register Inert and Vision plugins, we’ll add the code below (in the index.js file).

// vision registration
server.register(Vision, (err) => {
    if (err) throw err
})

// inert registration
server.register(Inert, (err) => {
    if (err) throw err
})

2.2. Hosting Static Contents

We need to add some routes to host some static files like images, css styles, scripts, etc.

Inside the index.js file we’ll make the call to the routes.

// routes for static contents css / scripts / images / etc
server.route(require(routesPath + 'staticRoutes'))

Inside the routes directory we need to create a file called staticRoutes.js with the content below.

const websitePath = './website/'
module.exports = [
    {
        method: 'GET',
        path: '/css/{file}',
        handler: (request, reply) => {
            reply.file(websitePath + 'css/' + request.params.file)
        }
    },
    {
        method: 'GET',
        path: '/script/{file}',
        handler: (request, reply) => {
            reply.file(websitePath + 'script/' + request.params.file)
        }
    },
    {
        method: 'GET',
        path: '/image/{file}',
        handler: (request, reply) => {
            reply.file(websitePath + 'img/' + request.params.file)
        }
    }
]

2.3. Website Template

Now we can focus on building the website part, for that we need to enable the rendering of templates, so inside the index.js file we need to add the code below.

// set the views
server.views({
    engines: {
        html: Handlebars
    },
    path: 'website/contents',
    layoutPath: 'website',
    layout: 'index',
    partialsPath: 'website/partials',
    helpersPath: 'website/helpers'
})

2.3.1. Directory Structure

According to the view we’ve just configured, and to the static contents we routed, we need to create a few directories inside the website directory.

Create a directory called contents to store many page templates for the dynamic call from the routes we’ll build in the next chapters.

Create a directory called css to store the styles for your website.

Create a directory called helpers to store functions for dynamic rendering/data.

Create a directory called img to store the images you want to use on your website.

Create a directory called partials to store pieces of html and codes to be used multiple times in your website.

Create a directory called script to store javascript codes you want to use on your website.

2.3.2. Main Layout

In the chapter 2.3. Website Template we configured our template rendering and set the main layout to be a file called index, so we need to create a html file index.html and this file must be at the website directory as configured on layoutPath.

I’ll make this example as simple as possible, so it will be easy to understand. Inside the index.html file let’s add the code below.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">

    <link rel="stylesheet" href="{{thisURL}}css/style.css">
    <title>My Site</title>
</head>
<body>
    <header>
        {{> header}}
    </header>

    <div class="main-container">
        {{{content}}}
    </div>

    <footer>
        {{> footer}}
    </footer>
</body>
<script src="{{thisURL}}script/script.js"></script>
</html>

You can already notice that this content has some different features than just simple HTML elements, so let me explain each of them.

2.3.3. Partial Layouts

For the partial header layout, I want to create a simple menu and display the title of the page. Inside the partials directory just create a header.html file and add the code below.

<nav class="main-navigation">
    <div class="button-menu-container">
        <button id="btnMenu" class="icon-menu">Menu</button>
    </div>
    <ul class="top-menu menu-slide-vertical">
        {{#each menu}}
            <li>
                <a href="{{thisURL}}{{this.urlSuffix}}">{{this.name}}</a>
                {{#if this.subItems}}
                    <button class="icon-keyboard_arrow_down">Sub Menu</button>
                    <ul class="sub-menu sub-menu-projects">
                        {{#each this.subItems}}
                            <li><a href="{{thiURL}}{{this.urlSuffix}}">{{this.name}}</a></li>
                        {{/each}}
                    </ul>
                {{/if}}
            </li>
        {{/each}}
    </ul>
</nav>
<div class="header-page">
    <h1>Site Title</h1>
    <p>Simple Quote</p>
</div>

Here we have some more new features, and those ones are quite useful.

For the partial footer layout, it will only repeat the links and add some social links as well. Create a file called footer.html and add the code below.

<div class="footer-page">
    <nav class="footer-navigation">
        <p>Links:</p>
        <ul class="bottom-menu">
            {{#each menu}}
                <li>
                    <a href="{{thisURL}}{{this.urlSuffix}}">{{this.name}}</a>
                </li>
            {{/each}}
        </ul>
    </nav>
    <nav class="footer-social">
        <p>My social profiles:</p>
        <ul class="social-menu bottom-social">
            {{#each social}}
                <li>
                    <a href="{{this.url}}"><span class="visually-hidden">{{this.name}}</span></a>
                </li>
            {{/each}}
        </ul>
    </nav>
</div>

We’ll also create the social array later in chapter 2.4. Setting The Routes.

2.3.4. Helpers

In this example we’re just using the thisURL helper, so create a file called thisURL.js inside the helpers directory and add the code below.

module.exports = () => {
    return "http://localhost:3000/"
}

This is just a simple function that returns a string, but you can customize it to process any data and return your way.

2.3.5. Dynamic Content

These contents are called by the server route whenever it responds with the view function. For this example we’ll create 4 contents, but it’s up to you how many different layout structures you want to add in your website.

Create a file called front-page.html inside contents directory with the code below.

<div>
    <h2>Front Page</h2>
</div>

Create a file called about.html inside contents directory with the code below.

<div>
    <h2>About</h2>
    <p>Made in NodeJS</p>
    <p>Main NPM package: HapiJS</p>
</div>

Create a file called post.html inside contents directory with the code below.

<div>
    <h2>{{postContent.title}}</h2>
    <p>{{postContent.text}}</p>
</div>

Create a file called 404.html inside contents directory with the code below.

<div>
    <h2>404 Not Found :(</h2>
</div>

2.4. Setting The Routes

Now that we created our templates let’s set up the routes for this website.

In the root of the working directory, add the code below at the bottom of the index.js file.

// routes for the website
server.route(require(routesPath + 'pagesRoutes'))

// start serving
server.start((err) => {
    if (err) throw err

    console.log('server listening at: ', server.info.uri)
})

Create a file called pagesRoutes.js inside routes directory and add the code below.

"use strict"
const menuJSON = require('../data/menu.json')
const socialJSON = require('../data/social_links.json')
const postOne = require('../data/postOne.json')

module.exports = [
    {
        method: 'GET',
        path: '/',
        handler: (request, reply) => {
            return reply.view('front-page', {menu: menuJSON, social: socialJSON})
        }
    },
    {
        method: 'GET',
        path: '/about',
        handler: (request, reply) => {
            return reply.view('about', {menu: menuJSON, social: socialJSON})
        }
    },
    {
        method: 'GET',
        path: '/post/postOne',
        handler: (request, reply) => {
            return reply.view('post', {menu: menuJSON, social: socialJSON, postContent: postOne})
        }
    },
    {
        method: 'GET',
        path: '/{path*}',
        handler: (request, reply) => {
            return reply.view('404', {menu: menuJSON, social: socialJSON}).code(404)
        }
    }
]

We are basically routing 4 paths here:

The menu content is basically a file called menu.json inside data directory with the code below.

[
    {
        "name": "Home",
        "urlSuffix": ""
    },
    {
        "name": "Posts",
        "urlSuffix": "",
        "subItems": [
            {
                "name": "Post 1",
                "urlSuffix": "post/postOne"
            }
        ]
    },
    {
        "name": "About",
        "urlSuffix": "about"
    }
]

The social_links.json file will be like this.

[
    {
        "name": "facebook",
        "url": "https://www.facebook.com/user/"
    },
    {
        "name": "instagram",
        "url": "https://www.instagram.com/user/"
    },
    {
        "name": "linkedin",
        "url": "https://www.linkedin.com/in/user/"
    }
]

And the postOne.json will be like this.

{
    "title": "Title of the POST ONE!",
    "text": "It worked! YEAHHH!"
}

3. Testing Locally

So, now we’re all set let’s run the application!

Back to the terminal command start the index.js file.

> Node index.js
server listening at: http://localhost:3000

Open a browser and go to the URL displayed.

Well, of course the website is all without any style, and it might be displaying 2 errors on your console (style.css not found and script.js not found). Just remember that the server is hosting the directory css, so you can create a file style.css and make your own styles, and you can also create script.js inside script directory and make your web page scripts.

Whenever you need to add images into your page, add them inside img directory and call them using href="{{thisURL}}image/<your_image>".

So, it’s all up to you to customize and style and add your contents to your website. The core functionality is already working!

To deploy it into a server, you can search for any “server processing” solution. I like the Heroku one, but Amazon AWS and Google Cloud also have clusters to process your apps.

4. Conclusion

The usage of this solution allows you to have your own customized templates for your website. You can even start to think about evolving this to create a content management system, and after finishing all the configurations and styles for it you only need to focus on creating the contents.

The problem is that it will consume a lot of your time and effort to create all these templates, styles, and scripts to run on your customized website.

It’s up to you to balance the amount of effort you want to spend on building all this architecture until you finally reach something good enough to deploy on a real server/cloud, or you can try to search for another solution on the web like Hugo.

5. Resources

[Android] Creating Custom Login Screen for AWS Mobile Hub

November 20, 2017
aws android howto login mobilehub

Creating and Hosting a Wordpress Site (Using AWS EC2 & RDS)

November 2, 2017
aws ec2 howto rds wordpress

Calling an API on Android Studio

September 19, 2017
android api howto java