A node cli project static site generator

So for todays node cli project I started working on a basic static site generator, one of many project ides of node cli tool examples. The project makes use of the npm package known as marked which can be used to parse markdown files into html, as well as some of my other node cli projects such as nc-walk that make part of my node cli tools repository project. This static site generator might not really be of production quality as of yet, but if I do put more time into this project I am sure it will get there.

1 - the node cli tools project

this is a post on the nc-ssg command for my node cli tools project. I will not be getting into the full depth of the project as a whole here, but I will say that it is a project that is a collection of node cli tools that can be used to create and maintain a website.

2 - The node cli /bin/ssg folder

In the bin folder of the node cli tools project I creates a folder called ssg. This folder will contain the main file that will be called when the nc-bin command is called. In this file I am using yargs to parse options that are passed when calling the command.

1
2
3
4
5
6
#!/usr/bin/env node
require('yargs')
.command(require('./commands/default.js'))
.command(require('./commands/gen.js'))
.argv;

3 - The /bin/ssg/commands folder

This folder is a common folder that I have for all the commands of my node_cli_tools project. some commands might have more that one sub command and although that is not yet the case for nc-ssg, it might be the case at some point in the future for this one if I do continue working on this project. So far there is a default command, and a gen command which is short for generate. Future sub commands might be something like watch, which will automatically generate each time changes are made t the sites content.

3.1 - default.js

Here I have just the logic for the default command that for now just logs to the console how to generate a site folder.

1
2
3
4
5
6
7
exports.command = '*';
exports.describe = 'default command';
exports.handler = function (argv) {
console.log('nc-ssg:');
console.log('use gen sub-command to generate a public folder when in the root working path of a project folder.');
console.log('$ nc-ssg gen');
};

3.2 - gen.js

Here I have the logic for the sub command that will be used to generate a public folder with the posts and theme of a site folder that was created with the nc-init command.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
let path = require('path'),
gen = require('../lib/gen.js');
exports.command = 'gen';
exports.aliases = ['g'];
exports.describe = 'generate command';
exports.builder = {
// root project path
r: {
default:process.cwd()
},
// path to create the public folder in
p: {
default:path.join(process.cwd(), '_public')
},
// dir to folder of the theme to use
e:{
default: path.join(process.cwd(), '_themes/core')
}
};
exports.handler = function (argv) {
gen({
dir_root: argv.r,
dir_public: argv.p,
dir_theme: argv.e
});
};

The script requires in another gen.js file that is in the lib folder of this command, that is where I have most of my logic that composes the ssg.

4 - The /bin/ssg/lib folder

So with some commands I might have a lib folder that is local to the folder of the command in the bin folder. This local lib folder is where I might park all kinds of files that are close to the command, rather than modules that might be used by more that one command in the project. Here I have a file that is used with my walk.js module to generate posts, as well as a gen.js file that contains the bulk of the logic for the static site generator at this time.

4.1 - for_post.js

Here I have the file that exports a method that will be used to generate each page for each blog post in the _posts folder of the site folder that was created with the nc-init command. This method is used with my walk.js module that is in the shared folder of the node_cli_tools project.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
let fs = require('fs'),
path = require('path'),
promisify = require('util').promisify,
readFile = promisify(fs.readFile),
writeFile = promisify(fs.writeFile),
marked = require('marked');
// is markdown helper
let isMarkdown = (item) => {
// is the item a markdown file?
if (item.stat.isFile && item.fileName.match(/.md$/)) {
return Promise.resolve()
} else {
return Promise.reject(new Error('item in posts folder is not a markdown file'));
}
}
// main forFile method to be used with nc-walk
module.exports = (api, item, next) => {
console.log('generating post files for public folder: ' + api.dir_public);
// the dir for the new html file
let dir_html = path.join( api.dir_public, path.basename(item.fileName, '.md') + '.html' );
// is the item markdown?
isMarkdown(item)
// read the markdown file
.then(() => {
return readFile(item.path)
})
// use marked to convert post to html
// and write the new html file in the public folder
.then((data) => {
let html = marked(data.toString());
// write the file
//return writeFile(dir_html, html, 'utf8');
return api.render({
layout: 'post',
path: '/blog',
content: html
});
next();
})
// then log gen file message
.then(()=>{
//console.log('gen: ');
//console.log(api);
})
// if and error happens
.catch((e) => {
console.log(e);
next();
})
};

4.2 - gen.js

Here I have the current state of gen.js of the nc-ssg command. I am making use of ejs as a template system that is one of the npm packages that I have made as part of the stack for node_cli_tools.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
let ejs = require('ejs'),
fs = require('fs'),
promisify = require('util').promisify,
mkdirp = promisify(require('mkdirp')),
renderFile = promisify(ejs.renderFile),
writeFile = promisify(fs.writeFile),
path = require('path'),
walk = require('../../../shared/lib/walk/walk.js');
// create a render method that will be used to generate all html files
// the root dir of the theme, and locals object will be closed over
// and a render method will be retruned where only a custom tailer object
// is passed as the one argument that chainges things like layout,
// the current post to render and so forth.
let createRenderMethod = (conf) => {
// main index.ejs template file location
let path_template_index = path.join(conf.dir_theme, 'index.ejs'),
// ejs options
ejs_options = {
root: conf.dir_theme
},
// the locals object
ejs_locals = {
conf: conf,
title: 'site_foo main index',
currentPage:{}
};
// return a resolved Promise with the render method
return Promise.resolve(function(pageInfo){
// update currentPage info default values
// will result in a main index.html build
pageInfo = pageInfo || {};
ejs_locals.currentPage = Object.assign({},{
layout: 'home',
path: '/',
content: '',
fileName: 'index.html'
}, pageInfo);
// use ejs renderFile promisifyed to create html
return renderFile( path_template_index, ejs_locals, ejs_options )
// we now have html that can be saved
.then((html)=>{
// write the html file to the public folder
let dir_target = path.join(ejs_locals.conf.dir_public, ejs_locals.currentPage.path),
path_target = path.join(dir_target, ejs_locals.currentPage.fileName);
// ensure dir for file
return mkdirp(dir_target)
// write the file
.then(()=>{
return writeFile(path_target, html, 'utf8');
})
.then(()=>{
console.log('\u001b[36m > render: ' + path_target + '\u001b[39m');
});
});
});
};
// generate posts
let genPosts = (opt, render) => {
let path_script = path.resolve(__dirname, '../lib/for_post.js'),
path_target = path.resolve(opt.dir_root, '_posts');
console.log('generating blog posts...');
// walk _posts
walk.walk({
dir: path_target,
forFile: require(path_script),
api: {
dir_posts: path_target,
dir_public: opt.dir_public,
render: render
}
});
};
let genIndex = (opt, render) => {
console.log('building main index file uisng theme at:');
console.log(opt.dir_theme);
let path_template_index = path.join(opt.dir_theme, 'index.ejs'),
ejs_locals = {
conf: opt,
title: 'site_foo main index'
},
ejs_options = {root: opt.dir_theme};
//renderFile( path_template_index, ejs_locals, ejs_options )
render()
.catch((e)=>{
console.log('error building /index.html');
console.log(e.message);
});
};
// exported method for gen.js
module.exports = (opt) => {
let render = function(){};
// make sure public folder is there
mkdirp(opt.dir_public)
.then(()=>{
return createRenderMethod(opt);
})
.then((newRenderMethod)=>{
render = newRenderMethod;
genIndex(opt, render);
})
// gen posts
.then(() => {
genPosts(opt, render);
})
// if error
.catch((e) => {
console.log(e);
});
};