You have successfully created a flat filesystem Content Management System (CMS) using Go. The next step is to take the same ideal and make a web server using Node.js. I will show you how to load the libraries, create the server, and run the server.
This CMS will use the site data structure as laid out in the first tutorial, Building a CMS: Structure and Styling. Therefore, download and install this basic structure in a fresh directory.
Getting Node and the Node Libraries
The easiest way to install Node.js on a Mac is with Homebrew. If you haven’t installed Homebrew yet, the tutorial Homebrew Demystified: OS X’s Ultimate Package Manager will show you how.
To install Node.js with Homebrew, type this instruction into a terminal:
brew install node
When done, you will have node and npm commands fully installed on your Mac. For all other platforms, follow the instructions on the Node.js website.
Be careful: Many package managers are currently installing Node.js version 0.10. This tutorial is assuming that you have version 5.3 or newer. You can check your version by typing:
node --version
The node
command runs the JavaScript interpreter. The npm
command is a package manager for Node.js to install new libraries, create new projects, and run scripts for a project. There are many great tutorials and courses on Node.js and NPM at Envato Tuts+.
To install the libraries for the web server, you have to run these commands in the Terminal.app or iTerm.app program:
npm install express --save npm install handlebars --save npm install moment --save npm install marked --save npm install jade --save npm install morgan --save
Express is a web application development platform. It is similar to the goWeb library in Go. Handlebars is the templating engine for creating the pages. Moment is a library for working with dates. Marked is a great Markdown to HTML converter in JavaScript. Jade is an HTML shorthand language for easily creating HTML. Morgan is a middleware library for Express that generates the Apache Standard Log Files.
An alternative way to install the libraries is to download the source files for this tutorial. Once downloaded and unzipped, type this in the main directory:
npm --install
That will install everything needed to create this project.
nodePress.js
Now you can get started creating the server. In the top directory of the project, create a file called nodePress.js, open it in your editor of choice, and start adding the following code. I am going to explain the code as it is placed into the file.
// // Load the libraries used. // var fs = require('fs'); var path = require("path"); var child_process = require('child_process'); var process = require('process'); var express = require('express'); // http://expressjs.com/en/ var morgan = require('morgan'); // https://github.com/expressjs/morgan var Handlebars = require("handlebars"); // http://handlebarsjs.com/ var moment = require("moment"); // http://momentjs.com/ var marked = require('marked'); // https://github.com/chjj/marked var jade = require('jade'); // http://jade-lang.com/
The server code starts with the initialization of all the libraries used to make the server. Libraries that do not have a comment with a web address are internal Node.js libraries.
// // Setup Global Variables. // var parts = JSON.parse(fs.readFileSync('./server.json', 'utf8')); var styleDir = process.cwd() + '/themes/styling/' + parts['CurrentStyling']; var layoutDir = process.cwd() + '/themes/layouts/' + parts['CurrentLayout']; var siteCSS = null; var siteScripts = null; var mainPage = null;
Next, I set up all the global variables and library configurations. The use of global variables is not the best software design practice, but it does work and makes for quick development.
The parts
variable is a hash array containing all the parts of a web page. Every page references the contents of this variable. It begins with the contents of the server.json file found at the top of the server directory.
I then use the information from the server.json file to create the complete paths to the styles
and layouts
directories used for this site.
Three variables are then set to null values: siteCSS
, siteScripts
, and mainPage
. These global variables will contain all the CSS, JavaScripts, and main index page contents. These three items are the most requested items on any web server. Therefore, keeping them in memory saves time. If the Cache
variable in the server.json file is false, these items get re-read with every request.
marked.setOptions({ renderer: new marked.Renderer(), gfm: true, tables: true, breaks: false, pedantic: false, sanitize: false, smartLists: true, smartypants: false });
This block of code is for configuring the Marked library for generating HTML from Markdown. Mostly, I am turning on table and smartLists support.
parts["layout"] = fs.readFileSync(layoutDir + '/template.html', 'utf8'); parts["404"] = fs.readFileSync(styleDir + '/404.html', 'utf8'); parts["footer"] = fs.readFileSync(styleDir + '/footer.html', 'utf8'); parts["header"] = fs.readFileSync(styleDir + '/header.html', 'utf8'); parts["sidebar"] = fs.readFileSync(styleDir + '/sidebar.html', 'utf8'); // // Read in the page parts. // var partFiles = fs.readdirSync(parts['Sitebase'] + "parts/"); partFiles.forEach(function(ele, index, array) { parts[path.basename(ele, path.extname(ele))] = figurePage(parts['Sitebase'] + "parts/" + path.basename(ele, path.extname(ele))); });
The parts
variable is further loaded with the parts from the styles
and layout
directories. Every file in the parts
directory inside the site
directory is also loaded into the parts
global variable. The name of the file without the extension is the name used to store the contents of the file. These names get expanded in the Handlebars macro.
// // Setup Handlebar's Helpers. // // // HandleBars Helper: save // // Description: This helper expects a // "<name>" "<value>" where the name // is saved with the value for future // expansions. It also returns the // value directly. // Handlebars.registerHelper("save", function(name, text) { // // Local Variables. // var newName = "", newText = ""; // // See if the name and text is in the first argument // with a |. If so, extract them properly. Otherwise, // use the name and text arguments as given. // if(name.indexOf("|") > 0) { var parts = name.split("|"); newName = parts[0]; newText = parts[1]; } else { newName = name; newText = text; } // // Register the new helper. // Handlebars.registerHelper(newName, function() { return newText; }); // // Return the text. // return newText; }); // // HandleBars Helper: date // // Description: This helper returns the date // based on the format given. // Handlebars.registerHelper("date", function(dFormat) { return moment().format(dFormat); }); // // HandleBars Helper: cdate // // Description: This helper returns the date given // in to a format based on the format // given. // Handlebars.registerHelper("cdate", function(cTime, dFormat) { return moment(cTime).format(dFormat); });
The next section of code defines the Handlebars helpers that I defined for use in the web server: save
, date
, and cdate
. The save helper allows for the creation of variables inside a page. This version supports the goPress version where the parameter has the name and value together separated by a “|”. You can also specify a save using two parameters. For example:
{{save "name|Richard Guay"}} {{save "newName" "Richard Guay"}} Name is: {{name}} newName is: {{newName}}
This will produce the same results. I prefer the second approach, but the Handlebars library in Go doesn’t allow for more than one parameter.
The date
and cdate
helpers format the current date (date
) or a given date (cdate
) according to the moment.js library formatting rules. The cdate
helper expects the date to render to be the first parameter and have the ISO 8601 format.
// // Create and configure the server. // var nodePress = express(); // // Configure middleware. // nodePress.use(morgan('combined'))
Now, the code creates an Express instance for configuring the actual server engine. The nodePress.use()
function sets up the middleware software. Middleware is any code that gets served on every call to the server. Here, I set up the Morgan.js library to create the proper server log output.
// // Define the routes. // nodePress.get('/', function(request, response) { setBasicHeader(response); if((parts["Cache"] == true) && (mainPage != null)) { response.send(mainPage); } else { mainPage = page("main"); response.send(mainPage); } }); nodePress.get('/favicon.ico', function(request, response) { var options = { root: parts['Sitebase'] + 'images/', dotfiles: 'deny', headers: { 'x-timestamp': Date.now(), 'x-sent': true } }; response.set("Content-Type", "image/ico"); setBasicHeader(response); response.sendFile('favicon.ico', options, function(err) { if (err) { console.log(err); response.status(err.status).end(); } else { console.log('Favicon was sent:', 'favicon.ico'); } }); }); nodePress.get('/stylesheets.css', function(request, response) { response.set("Content-Type", "text/css"); setBasicHeader(response); response.type("css"); if((parts["Cache"] == true) && (siteCSS != null)) { response.send(siteCSS); } else { siteCSS = fs.readFileSync(parts['Sitebase'] + 'css/final/final.css'); response.send(siteCSS); } }); nodePress.get('/scripts.js', function(request, response) { response.set("Content-Type", "text/javascript"); setBasicHeader(response); if((parts["Cache"] == true) && (siteScripts != null)) { response.send(siteScripts); } else { siteScripts = fs.readFileSync(parts['Sitebase'] + 'js/final/final.js', 'utf8'); response.send(siteScripts); } }); nodePress.get('/images/:image', function(request, response) { var options = { root: parts['Sitebase'] + 'images/', dotfiles: 'deny', headers: { 'x-timestamp': Date.now(), 'x-sent': true } }; response.set("Content-Type", "image/" + path.extname(request.params.image).substr(1)); setBasicHeader(response); response.sendFile(request.params.image, options, function(err) { if (err) { console.log(err); response.status(err.status).end(); } else { console.log('Image was sent:', request.params.image); } }); }); nodePress.get('/posts/blogs/:blog', function(request, response) { setBasicHeader(response); response.send(post("blogs", request.params.blog, "index")); }); nodePress.get('/posts/blogs/:blog/:post', function(request, response) { setBasicHeader(response); response.send(post("blogs", request.params.blog, request.params.post)); }); nodePress.get('/posts/news/:news', function(request, response) { setBasicHeader(response); response.send(post("news", request.params.news, "index")); }); nodePress.get('/posts/news/:news/:post', function(request, response) { setBasicHeader(response); response.send(post("news", request.params.news, request.params.post)); }); nodePress.get('/:page', function(request, response) { setBasicHeader(response); response.send(page(request.params.page)); });
This section of code defines all the routes needed to implement the web server. All routes run the setBasicHeader()
function to set the proper header values. All requests for a page type will evoke the page()
function, while all requests for post type page will evoke the posts()
function.
The default for Content-Type
is HTML. Therefore, for CSS, JavaScript, and images, the Content-Type
is explicitly set to its appropriate value.
You can also define routes with the put
, delete
, and post
REST verbs. This simple server only makes use of the get
verb.
// // Start the server. // var addressItems = parts['ServerAddress'].split(':'); var server = nodePress.listen(addressItems[2], function() { var host = server.address().address; var port = server.address().port; console.log('nodePress is listening at http://%s:%s', host, port); });
The last thing to do before defining the different functions used is to start the server. The server.json file contains the DNS name (here, it is localhost
) and the port for the server. Once parsed, the server’s listen()
function uses the port number to start the server. Once the server port is open, the script logs the address and port for the server.
// // Function: setBasicHeader // // Description: This function will set the basic header information // needed. // // Inputs: // response The response object // function setBasicHeader(response) { response.append("Cache-Control", "max-age=2592000, cache"); response.append("Server", "nodePress - a CMS written in node from Custom Computer Tools: http://customct.com."); }
The first function defined is the setBasicHeader()
function. This function sets the response header to tell the browser to cache the page for one month. It also tells the browser that the server is a nodePress server. If there are any other standard header values you want, you would add them here with the response.append()
function.
// // Function: page // // Description: This function processes a page request // // Inputs: // page The requested page // function page(page) { // // Process the given page using the standard layout. // return (processPage(parts["layout"], parts['Sitebase'] + "pages/" + page)); }
The page()
function sends the layout template for a page and the location of the page on the server to the processPage()
function.
// // Function: post // // Description: This function processes a post request // // Inputs: // type The type of post. // cat The category of the post. // post The requested post // function post(type, cat, post) { // // Process the post given the type and the post name. // return (processPage(parts["layout"], parts['Sitebase'] + "posts/" + type + "/" + cat + "/" + post)); }
The post()
function is just like the page()
function, except that posts have more items to define each post. In this series of servers, a post contains a type
, category, and the actual post
. The type is either blogs
or news
. The category is flatcms
. Since these represent directory names, you can make them whatever you want. Just match the naming to what is in your file system.
// // Function: processPage // // Description: This function processes a page for the CMS. // // Inputs: // layout The layout to use for the page. // page Path to the page to render. // function processPage(layout, page) { // // Get the pages contents and add to the layout. // var context = {}; context = MergeRecursive(context, parts); context['content'] = figurePage(page); context['PageName'] = path.basename(page, path.extname(page)); // // Load page data. // if(fileExists(page + ".json")) { // // Load the page's data file and add it to the data structure. // context = MergeRecursive(context, JSON.parse(fs.readFileSync(page + '.json', 'utf8'))); } // // Process Handlebars codes. // var template = Handlebars.compile(layout); var html = template(context); // // Process all shortcodes. // html = processShortCodes(html); // // Run through Handlebars again. // template = Handlebars.compile(html); html = template(context); // // Return results. // return (html); }
The processPage()
function gets the layout and the path to the page contents to render. The function starts by making a local copy of the parts
global variable and adding the “contents” hashtag with the results of calling figurePage()
function. It then sets the PageName
hash value to the name of the page.
This function then compiles the page contents to the layout template using Handlebars. After that, the processShortCodes()
function will expand all of the shortcodes defined on the page. Then, the Handlebars template engine goes over the code once more. The browser then receives the results.
// // Function: processShortCodes // // Description: This function takes a string and // processes all of the shortcodes in // the string. // // Inputs: // content String to process // function processShortCodes(content) { // // Create the results variable. // var results = ""; // // Find the first match. // var scregFind = /\-\[([^\]]*)\]\-/i; var match = scregFind.exec(content); if (match != null) { results += content.substr(0,match.index); var scregNameArg = /(\w+)(.*)*/i; var parts = scregNameArg.exec(match[1]); if (parts != null) { // // Find the closing tag. // var scregClose = new RegExp("\\-\\[\\/" + parts[1] + "\\]\\-"); var left = content.substr(match.index + 4 + parts[1].length); var match2 = scregClose.exec(left); if (match2 != null) { // // Process the enclosed shortcode text. // var enclosed = processShortCodes(content.substr(match.index + 4 + parts[1].length, match2.index)); // // Figure out if there were any arguments. // var args = ""; if (parts.length == 2) { args = parts[2]; } // // Execute the shortcode. // results += shortcodes[parts[1]](args, enclosed); // // Process the rest of the code for shortcodes. // results += processShortCodes(left.substr(match2.index + 5 + parts[1].length)); } else { // // Invalid shortcode. Return full string. // results = content; } } else { // // Invalid shortcode. Return full string. // results = content; } } else { // // No shortcodes found. Return the string. // results = content; } return (results); }
The processShortCodes()
function takes the web page contents as a string and searches for all shortcodes. A shortcode is a block of code similar to HTML tags. An example would be:
-[box]- <p>This is inside a box</p> -[/box]-
This code has a shortcode for box
around an HTML paragraph. Where HTML uses <
and >
, shortcodes use -[
and ]-
. After the name, a string containing arguments to the shortcode can or cannot be there.
The processShortCodes()
function finds a shortcode, gets its name and arguments, finds the end to get the contents, processes the contents for shortcodes, executes the shortcode with the arguments and contents, adds the results to the finished page, and searches for the next shortcode in the rest of the page. The looping is performed by recursively calling the function.
// // Define the shortcodes function array. // var shortcodes = { 'box': function(args, inside) { return ("<div class='box'>" + inside + "</div>"); }, 'Column1': function(args, inside) { return ("<div class='col1'>" + inside + "</div>"); }, 'Column2': function(args, inside) { return ("<div class='col2'>" + inside + "</div>"); }, 'Column1of3': function(args, inside) { return ("<div class='col1of3'>" + inside + "</div>"); }, 'Column2of3': function(args, inside) { return ("<div class='col2of3'>" + inside + "</div>"); }, 'Column3of3': function(args, inside) { return ("<div class='col3of3'>" + inside + "</div>"); }, 'php': function(args, inside) { return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: php'>" + inside + "</pre></div>"); }, 'js': function(args, inside) { return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: javascript'>" + inside + "</pre></div>"); }, 'html': function(args, inside) { return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: html'>" + inside + "</pre></div>"); }, 'css': function(args, inside) { return ("<div class='showcode'><pre type='syntaxhighlighter' class='brush: css'>" + inside + "</pre></div>"); } };
This next section defines the shortcodes
json structure that defines the name of a shortcode associated to its function. All shortcode functions accept two parameters: args
and inside
. The args
is everything after the name and space and before the closing of the tag. The inside
is everything contained by the opening and closing shortcode tags. These functions are basic, but you can create a shortcode to perform anything you can think of in JavaScript.
// // Function: figurePage // // Description: This function figures the page type // and loads the contents appropriately // returning the HTML contents for the page. // // Inputs: // page The page to load contents. // function figurePage(page) { var result = ""; if (fileExists(page + ".html")) { // // It's an HTML file. Read it in and send it on. // result = fs.readFileSync(page + ".html"); } else if (fileExists(page + ".amber")) { // // It's a jade file. Convert to HTML and send it on. I // am still using the amber extension for compatibility // to goPress. // var jadeFun = jade.compileFile(page + ".amber", {}); // Render the function var result = jadeFun({}); } else if (fileExists(page + ".md")) { // // It's a markdown file. Convert to HTML and send // it on. // result = marked(fs.readFileSync(page + ".md").toString()); // // This undo marked's URI encoding of quote marks. // result = result.replace(/\"\;/g,"\""); } return (result); }
The figurePage()
function receives the full path to a page on the server. This function then tests for it to be an HTML, Markdown, or Jade page based on the extension. I am still using .amber for Jade since that was the library I used with the goPress server. All Markdown and Jade contents get translated into HTML before passing it on to the calling routine. Since the Markdown processor translates all quotes to "
, I translate them back before passing it back.
// // Function: fileExists // // Description: This function returns a boolean true if // the file exists. Otherwise, false. // // Inputs: // filePath Path to a file in a string. // function fileExists(filePath) { try { return fs.statSync(filePath).isFile(); } catch (err) { return false; } }
The fileExists()
function is a replacement for the fs.exists()
function that used to be a part of the fs
library of Node.js. It uses the fs.statSync()
function to try to get the status of the file. If an error happens, a false
is returned. Otherwise, it returns true
.
// // Function: MergeRecursive // // Description: Recursively merge properties of two objects // // Inputs: // obj1 The first object to merge // obj2 The second object to merge // function MergeRecursive(obj1, obj2) { for (var p in obj2) { try { // Property in destination object set; update its value. if (obj2[p].constructor == Object) { obj1[p] = MergeRecursive(obj1[p], obj2[p]); } else { obj1[p] = obj2[p]; } } catch (e) { // Property in destination object not set; create it and set its value. obj1[p] = obj2[p]; } } return obj1; }
The last function is the MergeRecursive()
function. It copies the second pass object into the first passed object. I make use of this to copy the main parts
global variable into a local copy before adding page-specific parts.
Running Locally
After saving the file, you can run the server with:
node nodePress.js
Alternatively, you can use the npm
script that is in the package.json file. You run npm scripts like this:
npm start
This will run the start
script that is inside the package.json file.
Point your web browser to http://localhost:8080
and you will see the page above. You might have noticed that I added more test code to the main page. All the changes to the pages are in the download for this tutorial. They're mostly just some minor tweaks to more completely test the functionality and to fit any differences from using different libraries. The most notable difference is that the Jade library doesn't use $
to name variables while Amber does.
Conclusion
Now you have exactly the same flat filesystem CMS in Go and Node.js. This only scratches the surface of what you can build with this platform. Experiment and try something new. That is the best part of creating your own web server.
Comments