Part of the Solid Foundations Learning Series
This is an in-depth learning series focused on a specific application: a JavaScript-based suite of single-page applications optimized for use in a microservice environment. We focus on telling the story of “why” and “how” it was built.
—
In Part 2 of our Solid Foundations Learning Series, we covered quite a bit of ground surrounding the discovery of Handlebars files, creating navigation based off of those files, and the routing back to the server based on those files.
Now we’re going to dive back into the Handlebars pages and find out how we’re getting content into the pages when they’re rendered by the server.
We don’t need no HTML
Let’s take a look at one of our Handlebars view pages again.
{{> header }} {{> navigation }} {{!-- triple curly braces for literal content --}} {{{data.contents}}} {{> footer }}
As you can see, we simply do not have any HTML markup in this file. The header, the navigation, and the footer are all partials-—which do have HTML markup, but nothing that anyone would actually call content. So how are we going to do this? The answer lies in using Markdown files for our content.
But why Markdown you may ask? And why in actual files?
The decision to use Markdown files came organically during the development of this application. One, there was no database used, and using the database for saving HTML markup has been done more than once for many content management systems over the years.
Second, Markdown is a quick and simple way to create rich content without the need for special editors and/or training for the author. Essentially, Markdown is just text with a few special characters to designate what should be compiled into HTML at a later time. I felt that by using Markdown, I could quickly add content to a page and change it as needed.
So how is this being done?
To work with Markdown on the server side, we’re going to add an NPM package called showdown
to our package.json file. Adding showdown to the application is done with the command:
npm install showdown --save
Once that ran, we now have the following in our package.json file:
"showdown": "^1.9.0"
In our services
directory, we have created a file called markdown.service.js
which will be our JavaScript module for finding Markdown files, reading the content into the server, rendering that content as HTML, and sending that content back to the routing module.
Here’s the contents of the file:
const showdown = require("showdown"); const fs = require("fs"); function loadContents(route) { let path = `./page-content/${route}.md`; let converter = new showdown.Converter(); if (fs.existsSync(path)) { // return a promise so that the html is there before rendering. return new Promise(function(resolve, reject) { fs.readFile(path, "utf8", (err, data) => { if (err) throw err; return resolve(converter.makeHtml(data)); }); }); } else { // return this promise in case there are files that do not have markdown content. return new Promise(function(resolve, reject) { let notFoundTemplate = ` <h2><i class="fa fa-exclamation-triangle text-warning"></i> 204 No Content</h2> <p>The server successfully processed the request, but is not returning any content.</p> <p>The content may be available again in the future.</p>`; return resolve(notFoundTemplate); }); } } module.exports = { LoadContents: loadContents };
From top to bottom let’s walk through this file and see what all is happening here.
First, we’re going to include the showdown
module for working with Markdown and Node’s own fs
module for working with the file system. Next, we’re going to create a function looking for a parameter called route
which as you remember from Part 1, we use to designate the naming convention of the Handlebars pages we’re looking for.
Inside we create a path for where the content will be found, including the route attached and with the Markdown extension attached. We’re going to dive off here for a moment and discuss the philosophy of why the route is used like this throughout the application.
What we’re trying to do here is doing things by convention instead of by ID or something similar. Since this is purely a file-based system, and most likely humans are going to be reading the files, I decided that if we use the names of items to designate them, it will be easier for the developer/user to know what they’re looking for. So the convention is as follows; if you have a Handlebars file called about.hbs
and you wish for content to be associated with that file, you can simply add a Markdown file called about.md
and the system will be able to associate one to the other during runtime. If no Markdown file is found, this function will catch that error and display a friendly message to the browser, and if no Handlebars file is found, the system will simply not find that file and display an error stating that fact. Because the navigation is based on found Handlebars files, generally this shouldn’t even happen because there is nothing to navigate to.
Now back to the loadcontents
function. As you can see, we’re using Node’s fs.existsSync(path)
function to determine if the file exists. If the file is not found, we are not going through the trouble of reading it and just drop down to a literal templated message stating that no content was found. Now if the path was found, we create a promise for this action. A promise was used for both the success and error of this function because we do not want the route to render until we have read the Markdown content and have returned a compiled version. That magic happens in this small section of code:
fs.readFile(path, "utf8", (err, data) => { if (err) throw err; return resolve(converter.makeHtml(data)); });
Take note that we set the buffer to utf8
so that we can create text instead of a binary read of the file. During the resolve of the promise, we call our Showdown converter function, pass in the read contents of the Markdown file and return HTML from the promise.
We have already talked about the else
section of the function where we return a promise based notFoundTempate
to the calling code.
At the end of the file, we create a module exports call and return a public accessor for theloadContents
function.
What happens next?
So back in Part 1, we talked about the routing for both the index page of the web application and for the specific page routes. In both of those GET
calls, we created an object literal called data
which will be added to the Handlebars page and will be available inside of the page to be bound and displayed as needed. Here is an example of the data
object again:
let data = { title: "Home", links: fileListing.CreatedFileList(), contents: "", user: user };
Notice that we have a property on the object called contents
which is not filled and set only to an empty string. That’s going to change soon. In the index.js
file we have gone and included the line of:
const markDown = require("../services/markdown.service");
This now gives us access to our LoadContents
function exposed from the markdown.service
file. Inside each GET
route for which we want to add content to a Handlebars view, we call the following function:
// LoadContents returns a promise markDown.LoadContents(route).then(function(html) { if (html) { data.contents = html; } res.render("pages/" + route, { data }); });
Remember we set LoadContents
to return a promise, so we’re pausing the GET
request while our file system function goes off, finds the content based off of the route passed in. Once the function returns, we then
take the response of the promise, determine if HTML was found (and there should be since we’re either returning the Markdown contents or an HTML marked up error) and we’re adding that to the contents
property. After the contents have been added to the data
object, the view is now rendered and sent to the browser. But wait you say, what if our Handlebars view doesn’t need to display Markdown content? That’s an easy answer, the contents
property simply won’t be used on the view.
When the Handlebars page is rendered, this section of the view is used to bind the Markdown generated HTML to the view.
{{!-- triple curly braces for literal content --}} {{{data.contents}}}
And this works?
Yes! By convention, this works great. When you have matching files, for example, an about
view and an about
Markdown file, you can easily merge the two together and create a fully featured page. The advantages of using the Markdown is that the author of the content does not need to know HTML and can simply write their content. Second, if the author does know HTML, it’s very easy to add additional functionality to the view, such as a form, tables, etc.
There is a downside for the moment. This is file based. Which means if you’re adding content to the site, you will be working with the applications file system. And if you’re turning site content on or off, or even deactivating pages or activating pages you will still be inside the sites directory. But there is a solution for this in another later article. For now, I feel that we have created an easy way to match content with a view and have successfully reached this goal.
Stay tuned!