A Static Blog Generator From Scratch
For a while I've been a fan of the idea of static site generators. They seemed like a great way to have a speedy and simple website that can be updated with just a few markdown files, a terminal command, and a quick upload to a server. I've tried a few.
I liked Jekyll with Github Pages, although I've never really used Ruby and don't plan to in the future.
I love the idea of Hugo and I've played around with the Go language. It's fun, but after getting it set up I never got around to getting it configured.
I looked at Gatsby after hearing a podcast interview with the creator. However, it seemed like it would be difficult and/or a lot of work to get a blog up and running. It did pique my interest in GraphQL, even though I decided not to use Gatsby to create my static site.
I even created static pages from a WordPress site, which was a real learning experience, but relied on a shaky plugin that got slower with each new post. Also, WordPress has a huge footprint that seems like overkill for some static pages.
Another requirement I have for a static site generator is that it has to run on my small Chromebook. This basically leaves me with JavaScript and Node, because installing new programming languages has proven to either a) use up my precious hard drive space or b) be difficult or impossible because Crouton Linux has some very eccentric quirks.
I came to the conclusion that the only static site generator I want is one that I made myself, to my own specifications. So I created the Stupid Simple Blog Generator. Right now, it's a <150 line JavaScript file (and three dependencies) that reads markdown files, processes the markdown into HTML, and then inserts the text into post templates. Then, it takes a list of the posts, sorts them in descending order, and creates a post list page. It also generates a new home page with the most recent posts.
##The Code
index.js
const fs = require('fs'); // Node file system
const showdown = require('showdown'); // Markdown processor
const prism = require('prismjs'); // Styling for code tags
const jsdom = require('jsdom'); // DOM emulator to process code tags
const {JSDOM} = jsdom;
// These variables are for creating post lists.
let posts = [];
let length = 0;
let counter = 0;
// Read the posts directory and get each file.
fs.readdir('./posts/', (err, files) => {
if(err) throw err;
length = files.length;
files.forEach(file => {
const fileName = file.substr(0, file.lastIndexOf('.'));
getContents(`./posts/${file}`, fileName);
});
if(length === 0) {
postList([]);
makeHomePage([]);
}
});
// Get the contents of each markdown file.
const getContents = (file, fileName) => {
fs.readFile(file, 'utf8', (err, contents) => {
if(err) throw err;
processMarkdown(contents, fileName);
});
};
/*
- The first three lines of the markdown files contain extra info.
Get the info and strip those out.
- Run the markdown through the converter.
- Then put that HTML into jsdom and look for code and image nodes.
- Process code nodes with Prism so we get pretty code highlighting.
- Give image tags an image class for custom CSS.
- Push each post to an array and update the counter.
- If the counter reaches the total number of posts, sort the posts
by date and create post list and home pages.
*/
const processMarkdown = (markdown, fileName) => {
const lines = markdown.split('\n');
const title = lines[0].split(':')[1].trim();
const date = new Date(lines[1].split(':')[1].trim());
const description = lines[2].split(':')[1].trim();
lines.splice(0, 4);
markdown = lines.join('\n');
const converter = new showdown.Converter();
const text = converter.makeHtml(markdown);
const dom = new JSDOM(text);
const codeNode = dom.window.document.querySelector('code');
const imgNode = dom.window.document.querySelector('img');
if(codeNode) {
const code = codeNode.textContent;
const name = codeNode.className;
const processed = prism.highlight(code, prism['languages'][name]);
codeNode.innerHTML = processed;
}
if(imgNode) {
imgNode.parentElement.className = 'image';
}
const data = {
title: title,
date: date,
description: description,
text: dom.serialize(),
dir: fileName
};
posts.push(data);
counter++;
if(counter === length) {
posts.sort((a, b) => {
return b.date - a.date;
});
postList(posts);
processHTML(posts);
makeHomePage(posts);
}
};
/* Insert dynamic content into the post template, add previous/next links,
and write the file to the static folder. */
const processHTML = (posts) => {
posts.forEach((data, i) => {
fs.readFile('./templates/post_template.html', 'utf8', (err, contents) => {
if(err) throw err;
const next = posts[i+1];
const previous = posts[i-1];
const html = contents
.replace(/{{ description }}/g, data.description)
.replace(/{{ title }}/g, data.title)
.replace(/{{ date }}/g, data.date.toLocaleDateString('en-US', {weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'}))
.replace(/{{ post }}/g, data.text)
.replace(/{{ next }}/g, next ? next.title : '')
.replace(/{{ nextLink }}/g, next ? `../${next.dir}/` : '')
.replace(/{{ previous }}/g, previous ? previous.title : '')
.replace(/{{ previousLink }}/g, previous ? `../${previous.dir}/` : '');
const dir = `./static/blog/posts/${data.dir}/`;
if(!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
fs.writeFile(`${dir}/index.html`, html, err => {
if(err) throw err;
console.log(`Created article "${data.title}" at /static/blog/${data.dir}/`);
});
});
});
};
/* For each post in the posts array, create HTML as a string, place
into the template, and write the file to the static directory. */
const postList = (posts) => {
fs.readFile('./templates/list_template.html', 'utf8', (err, contents) => {
if(err) throw err;
let postList = '';
posts.forEach(post => {
postList += `<time>${post.date.toLocaleDateString('en-US', {year: 'numeric', month: 'short', day: 'numeric'})}</time><a href="./posts/${post.dir}/">${post.title}</a>\n`
});
const html = contents.replace(/{{posts}}/g, postList);
fs.writeFile('./static/blog/index.html', html, err => {
if(err) throw err;
console.log(`Created blog index at /static/blog/`);
});
});
};
/* Add the three newest posts to the home template and write the file
to the static directory. */
const makeHomePage = (posts) => {
fs.readFile('./templates/home_template.html', 'utf8', (err, contents) => {
if(err) throw err;
let postList = '';
posts.forEach((post, i) => {
if(i < 3) {
postList += `<time>${post.date.toLocaleDateString('en-US', {year: 'numeric', month: 'short', day: 'numeric'})}</time><a href="./blog/posts/${post.dir}/">${post.title}</a>\n`
}
});
const html = contents.replace(/{{posts}}/g, postList);
fs.writeFile('./static/index.html', html, err => {
if(err) throw err;
console.log(`Created home page at /static/`);
});
});
};
post_template.html (The other two templates are similar.)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ title }} - Blog | Mary Knize</title>
<meta name="author" content="Mary Knize">
<meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/css/style.css" media="all">
<link rel="stylesheet" type="text/css" href="/css/prism.css" media="all">
</head>
<body>
<header>
<div id="name"><a href="/">Mary Knize</a></div>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/projects/">Projects</a></li>
<li><a href="/blog/" class="active">Blog</a></li>
<li><a href="https://github.com/captainpainway">Github</a></li>
</ul>
</nav>
</header>
<article>
<h1>{{ title }}</h1>
<time>{{ date }}</time>
{{ post }}
</article>
<footer>
<div class="previous">« <a href="{{ previousLink }}">{{ previous }}</a></div>
<div class="next"><a href="{{ nextLink }}">{{ next }}</a> »</div>
</footer>
</body>
</html>
I can run the file builder from my blog's directory by running npm start, but I wanted it to be easier, so I added this to my .bashrc file:
alias buildblog='npm start --prefix ~/Blog/'
Now I can run buildblog from anywhere and build my blog. The last thing to do is upload the static files to my server!
More code for this project can be found at github.