Module Core Session 7 - Creating the Landing Page Part 2 - Populate Inner Components

Goal

In Session 5 we have created the structure of our landing page with the combination of Express template engines, Pug, and Stylus. However, we have not yet populated the inner content. In this module we are going to do so.

Approaches

There are a few different approaches that we can take in order to populate the inner components:

  1. Add the content directly into the template, like what we have right now.

    This is the simplest approach, and strictly speaking, it fulfills our requirement. However, we will not be able to reuse the content nor the template. If we want flexibility we need better solutions.

  2. Add the content via Express View Engines.

    Since we can pass in values for substitution with Express templates (called locals in Express), we can load up the necessary inner components inside the route handler, and then pass them as parameters to the template.

    This approach is developer centric, since if we need to change the components we will need to change at the code level. For many applications this approach will suffice, unless more flexibility is needed. I.e. give content creators the ability to control the changing of components.

  3. Allow content creator to change the components.

    This is the most flexible approach. It provides hooks into the template system for the content developers to use. I.e. the content developers can decide which inner components that they want to add, remove, or modify.

    Obviously the downside is complexity. If your app doesn't need it, number 2 is a completely valid approach.

Let's take a look at each approach in turn.

Option 1: Adding Content Directly

Currently, the landing page has the following text directly embedded into the template file (the text are circled):

Landing Page - Embedded texts are circled

In the template, the text are directly written as:

doctype
html
  head
    title Index page
    link(rel='stylesheet', href='/css/main.css')
    // for mobile
    meta(name='viewport', content='width=device-width, initial-scale=1')/
  body
    // header section
    nav.top-nav
      div.container-fluid
        div.navbar-header
          a.navbar-brand(href='/') Header Section
    // body section
    div.container-fluid
      div.jumbotron Jumbotron
        div.sign-up Signup
      div.proposition Proposition
      div.benefits
        div.benefit Benefits 1
        div.benefit Benefits 2
        div.benefit Benefits 3
      div.topics
        div.topic Topics 1
        div.topic Topics 2
        div.topic Topics 3
      div.call-to-action Call To Action
        div.sign-up Signup
    // footer section
    div.footer Footer section

This is actually a valid approach for simple sites with simple pages. Pug has a built-in way of sharing template structures via Template Inheritance, so we can write all of the content in Pug.

However, as soon as you want to write some of the content in Markdown or other formats (even HTML), you will not be able to leverage Pug's Template Inheritance anymore. This comes into play surprisingly quickly, since Pug isn't really a content writing format. Even if you creating the site for a single person, chances are you'll want to look for doing content outside of Pug files, and that's where approaches #2 and #3 come in.

Option 2: Add Content Via Express Template Engine

The second approach is to leverage Express Template Engine, since it allows variable substitutions via res.render as follows:

// pass key / val into 
res.render(<template path>, { <key>: <val>, ... })

What we could do then is to refactor our template to take in variables in places of the embedded content:

doctype
html
  head
    title Index page
    link(rel='stylesheet', href='/css/main.css')
    // for mobile
    meta(name='viewport', content='width=device-width, initial-scale=1')/
  body
    // header section
    nav.top-nav
      div.container-fluid
        div.navbar-header
          a.navbar-brand(href='/') Header Section
    // body section
    div.container-fluid
      div.jumbotron !{jumbotron}
        div.sign-up Signup
      div.proposition !{proposition}
      div.benefits
        for benefit in benefits
          div.benefit !{benefit}
      div.topics
        for topic in topics
          div.topic !{topic}
      div.call-to-action !{callToAction}
        div.sign-up Signup
    // footer section
    div.footer Footer section

And we change the root route handler with the right list of values passed in:

app.get('/', function (req, res, next) {
  return res.render('index', {
    jumbotron: 'This is the Jumbotron Section',
    proposition: 'This is the Proposition Section',
    benefits: [
      'This is Benefit 1 Section',
      'This is Benefit 2 Section',
      'This is Benefit 3 Section'
    ],
    topics: [
      'This is Topic 1 Section',
      'This is Topic 2 Section',
      'This is Topic 3 Section'
    ],
    callToAction: 'This is Call To Action Section'
  });
});

Once we restart, we'll see the following:

Landing Page via Template Variable Substitution

Option 2b: Moving the Template Variables Out to Markdown Files

The above works for dynamic values generated from database or computation, but doesn't work well for content that's meant to be written by authors, since we don't want the authors to have to modify the code in order to change the content, so let's push the above further out to a directory structure.

Once we have the content files created, we can then change our root route handler to read the list of files into the expected template variables. To do so, we need to refactor our current code by separating the Markdown file reading code from the static file handler as follows:

  1. Install bluebird and fs-extra-promise so we can start using Promise as discussed in Session 6.

    npm install --save-dev @types/bluebird @types/fs-extra-promise
    npm install --save bluebird fs-extra-promise
    
  2. Import bluebird and fs-extra-promise

    import * as Promise from 'bluebird';
    import * as fs from 'fs-extra-promise';
    
  3. Extract out the Markdown file reading function as follows:

    function readMarkDownFile(filePath : string) : Promise<string> {
      var md = new MarkdownIt();
      return fs.readFileAsync(filePath, 'utf8') // comes from fs-extra-promise
        .then((data) => {
          return md.render(data);
        });
    }
    
  4. Also extract the raw file reading function as follows:

    function readRawFile(filePath : string) : Promise<Buffer> {
      return fs.readFileAsync(filePath);
    }
    
  5. Then we can rewrite our existing static handler as follows:

    function configurableStaticFileHandler(folderName : string) {
      return function staticFileHandler(req : express.Request, res : express.Response, next : express.NextFunction) {
        // 1 - identify which particular file that this request is asking for.
        // 2 - find that file, read it, and then serve out the result.
        // hello.html
        // /hello.html.
        // /hello.html => static/hello.html
        // /test.html => static/test.html
        // <url> => static/<url>
        var mappedPath = mapUrlToFilePath(req.url, folderName);
        switch (mappedPath) {
          case '.md':
            return readMarkDownFile(mappedPath)
              .then((data) => {
                return res.end(data);
              })
              .catch(next);
          default:
            return readRawFile(mappedPath)
              .then((data) => {
                return res.end(data);
              })
              .catch(next);
        }
      }
    }
    
  6. (Also delete some of the older code that we won't be using any more, like fooHandler, barHandler, and defaultHandler, as well as code that depend on them)

With the above, we are now ready to change our root handler to read from the list of markdown files. We just need one more function:

function readMarkDownFileList(filePathList : string[]) : Promise<string[]> {
  return Promise.map(filePathList, readMarkDownFile);
}

With this, we can change the root handler as:

app.get('/', function (req, res, next) {
  return readMarkDownFileList([
    './static/landing-page/jumbotron.md',
    './static/landing-page/proposition.md',
    './static/landing-page/benefits/benefit-1.md',
    './static/landing-page/benefits/benefit-2.md',
    './static/landing-page/benefits/benefit-3.md',
    './static/landing-page/topics/topic-1.md',
    './static/landing-page/topics/topic-2.md',
    './static/landing-page/topics/topic-3.md',
    './static/landing-page/call-to-action.md'
  ].map((filePath) => path.join(__dirname, filePath)))
    .then((markdownFiles) => {
      console.log('markdownFiles', markdownFiles)
      return res.render('index', {
        jumbotron: markdownFiles[0],
        proposition: markdownFiles[1],
        benefits: [
          markdownFiles[2],
          markdownFiles[3],
          markdownFiles[4]
        ],
        topics: [
          markdownFiles[5],
          markdownFiles[6],
          markdownFiles[7]
        ],
        callToAction: markdownFiles[8]
      })
    })
    .catch(next);
});

Save. Restart. Refresh browser, and you'll see the content of the files being shown:

Landing Page - Loading from files

Conclusion

We have successfully decoupled the content from the code with Option #2. With this change, content author can make changes to the content without worrying about having to change the code. However, there are still limitations:

It would be nice to be able to author pages without having to modify the routes, so we should also explore Option 3, which we will do in the next session.