Making a minimalist static site generator from scratch
This post will give some inspiration on how to build a minimalist static site generator in nodeJs that can be used without any client side javascript.
Recipe#
- Fetch your blog posts from an api.
- Turn your blog posts to html.
- Write your file to disk.
Prerequisites#
- nodejs installed on your machine
- a service that serves your blog posts as markdown
- a basic knowledge of javascript
Step one: fetch data#
This can be done using any service. I used contentful. The service.getContent
function should return your blog posts in JSON format.
const service = require('./lib/service.js');
const build = require('./lib/build-site.js');
async function init() {
const content = await service.getContent();
await buildSite(content);
}
init();
Step two: render site#
Inside build-site.js you might find yourself staring at something like this:
const { persistMarkup } = require('./utils/persist');
const Blog = require('./pages/blog');
const Dashboard = require('./pages/dashboard');
const BlogPost = require('./pages/blog-post');
const NotFound = require('./pages/404');
async function init(content) {
const operations = [
persistMarkup('./public/index.html', Dashboard(content)),
persistMarkup('./public/blog.html', Blog(content)),
persistMarkup('./public/404.html', NotFound(content)),
...content.posts.map(post =>
persistMarkup(`./public/blog/${post.fields.slug}.html`, BlogPost(post))
),
];
return Promise.all(operations);
}
module.exports = init;
Now we get data, we persist strings. The step inbetwixt is missing, namely functions that transform data into html-strings. For this we will use ghetto components. But first lets make a template for the reusable parts of the site:
function Main({ content, title }) {
return `
<html>
<title>${title}</title>
<body>
<header>
<h1>chasing rats</h1>
<ul>
<li><a href="index.html">home</a></li>
<li><a href="blog.html">blog</a></li>
</ul>
</content>
</header>
<main>${content}</main>
</body>
</html>
`;
};
Now we escape the cruel fate of duplicating the navigation menu on every page. The function wraps our content in the main skeleton of our site. On to the creation of the ghetto components:
const MainTemplate = require('../templates/main');
const { documentToHtmlString } = require('@contentful/rich-text-html-renderer');
function BlogPost(post) {
const creationDate = new Date(post.fields.creationDate).toISOString().substring(0,10);
return `
<div class="blog-post">
<h1>${post.fields.title}</h1>
<time>${creationDate}</time>
<content>
${documentToHtmlString(post.fields.content)}
</content>
</div>
`;
}
const render = post =>
MainTemplate({
title: post.fields.metaTitle,
content: BlogPost(post),
});
module.exports = render;
And the second stage has come to fruition. Let us rejoice for a while before we enter the third and final stage.
Step three: persist#
const fse = require('fs-extra');
const minify = require('html-minifier').minify;
function minifyHTML(markup) {
return minify(markup, {
collapseWhitespace: true,
removeAttributeQuotes: true,
sortAttributes: true,
sortClassName: true,
});
}
async function persistMarkup(path, content) {
const minified = minifyHTML(content);
await fse.remove(path);
return fse.outputFile(path, minified);
}
What now?#
The foundation has been layed, but the tower has yet to be built. Study the source code for the whole saga. Chapters that remain include:
- Adding inline and deferred style sheets.
- Handling syntax highlightning on code blocks.
- Deploying to netlify.