How to Build a Simple Static Site Generator Using Node.js

Build an awesome static site generator and Markdown-powered blog

Kartik Nair
Better Programming

--

Photo by Alex wong on Unsplash.

My blog was built using GatsbyJS, an awesome static site generator for React. Well, it was built using Gatsby, but I ended up switching to this project, as you’ll find out at the end. Gatsby was pretty easy to pick up and all I had to do was customize the gatsby-blog-starter to get something great.

But I was curious about how the generator worked, so I decided to try and build a simple, bare-bones static site generator using Node.js. Feel free to follow along with the code on GitHub. So let’s get into it.

Why Simple Static Site Generators?

I love static site generators a lot because they allow you to use whatever heavy/bloated framework you want, but the end result will still be just simple and light HTML and CSS. This gives a sense of freedom that we wouldn’t normally have if we were dealing with a create-react-app for example.

Just for this project, check out the Lighthouse scores:

I know, right? Pretty amazing. The only reason it didn’t ace it was the heavy fonts, but that’s fine because they add enough aesthetic value for me to keep them.

Setup

So let’s get started! Open up your command line, navigate to where you would like to make your project, and then use the following commands to create a new Node.js project (these are for Windows, but I’m sure you can translate them over to your OS):

mkdir node-ssg && cd node-ssgnpm init -y

Now we’re going to install some packages that are going to make our life a hell of a lot easier while dealing with the data. The packages are:

  • front-matter for extracting the YAML front matter from our posts.
  • marked for converting Markdown to HTML.
  • highlight.js for syntax highlighting in code.

We can install all of these using the following command:

npm i front-matter marked highlight.js

Alright, now we can start our project.

The Build Script

Let’s think about what we want to do first of all. So we want a build script that takes all the Markdown posts from the content directory and spits out static HTML files in the public directory.

First of all, let’s create the script that will run when we call build. We’ll put all our source code in the src directory, so go ahead and make that in the root of your project. Then open up the project in your favorite code editor (I’m using VS Code) and add in a JavaScript file called index.js. Now we can add our build script to our package.json simply by using node to run our index.js file. Your package.json should now look like this:

Great! Now we can call npm run build in our project and it’ll run our index.js file. The only problem is that our file doesn’t do anything yet.

For testing purposes, I’m going to create a content folder with my own blog posts. Since my current blog is also built using a static site generator, I can just copy my content folder from there.

Decisions to Make

Alright, there are many decisions you should make before starting. For example, how should posts be stored? Should they be stored in their own folder or just as .md file? Where do you store images? And a lot more.

But since this project’s scope is not very large, I’m going to go with a very simple file tree. All posts will be stored in the content directory as Markdown files (.md) and other resources (like images) can be stored in ./public/assets/. These decisions were made to make file reading and writing simpler for this specific scenario, but you can always change them to whatever is better suited for your project.

The config.js File

We can put the decisions we made in a config.js file so we can access it from anywhere by just requiring it. I put them in a dev object because there are other properties that we will add later. So this is how it looks right now:

Getting the Posts

Alright, let’s start with getting all the posts from the content directory. We can do this using the fs API that Node.js gives us. So first of all, we import fs and create an instance of it:

const fs = require(“fs”);

Now we can use the methods that fs provides in this syntax: fs.methodName(). For getting posts, we can use the readdirSync() method that fs provides. So let’s see how it would look if we just got all the posts and logged them to the console:

Now run npm run build in your console and you should see a list of posts if you did everything right. The reason we use slice() in the code is to get rid of the .md extension. You’ll see why we have to do that later on.

Parsing Post Markdown

If you remember, in the beginning, we installed an NPM package called front-matter. It helps us extract YAML front-matter from files. What is YAML front-matter? Well, it’s this amazing thing that lets you add extra YAML data to your files using — - before and after it to delimit it from the rest of the content. Here’s an example of a blog post using YAML front-matter:

---title: Post Onedate: “1583826864020”description: My reasons for starting a blog.---# This is an amazing blog post.Really it’s just great

Since we got the posts in the previous step, now we can parse them using front-matter. We’re going to put all of this post-related code in posts.js so we have a cleaner working environment. So let’s start off with getting the content from our files.

We can do that using the provided fs.readFile() method. Here’s how it would look just logging the content of the file to the console:

console.log(fs.readFileSync(“./foo.md”))

But since we want reusable code that we can use for every single post in a loop, we’ll put it in a function called createPost(). This function will use front-matter to take the content of the file and give us an object. This object will have the front-matter properties we set in a property called attributes and the rest of the content will be in a property called body. We can use front-matter by creating an instance to it using require and then calling it on our data once we read it from the file.

Here’s how that would look:

If you check out the code, you’ll see that I call marked on the body of our post. All this does is convert the Markdown into HTML so we can easily display it on our website later. I’ve also added the path of the post as an extra property because we will need it later on.

Now let’s use this method in index.js and just log the output:

Configuring marked and Syntax Highlighting

Since we would like to use highlight.js to highlight our code, we can do that using marked and its configuration object. Make a file called marked.js, and in that, we’ll create an instance of marked, configure it, and then export it. Here’s how that looks:

So now every time you use marked, require it from this file directly.

Generating Post HTML Pages

Now we start with the actual page generation. To start off, we want it to create the public folder. If it doesn’t exist already, we can do that using the fs.mkdirSync() and fs.existsSync() functions. Let’s add that to our index.js file:

if (!fs.existsSync(config.dev.outdir)) fs.mkdirSync(config.dev.outdir);

Now in our posts.js file, let us make a createPosts() function that will create and write the HTML files to the public directory. But before that, we need a helper function called posthtml that will take the post JSON object and return a complete HTML page that we can simply write to a file. We will use the power of template literals to make our life easier in this function. Here’s how it looks:

The reason I create a new Date() when adding the date to the post is so that all the dates have a consistent format. This is quite an opinionated way of doing it, as it requires the date provided in the front-matter to be a “number representing the milliseconds elapsed since the UNIX epoch.” However, I don’t mind running a quick Date.now() in the browser dev tools to get that number before I post. You can change that in the code if you would like.

Now we can create a function called createPosts() that’ll take the output of the createPost() function and generate an HTML file. Here’s how it looks:

As you can see, it doesn’t generate a file called postname.html but rather makes a directory called postname and then adds an index.html in that directory so that the path for that post in the browser will be yourwebsite/postname and not yourwebsite/postname.html.

Now let’s call it in index.js and see if it worked:

If everything worked right, you should’ve seen a public directory pop up with a few directories in it (based on how many posts you had).

About Section

This blog will also include a small About section on its homepage for the author, so we need to add the info for that into our config.js file. So here’s our revised config.js file:

The Homepage

The homepage will be the index.html file in the public directory. It should have a header with the blog’s name and a small About section for the author. We can use template literals like we did before to generate the HTML for that. Let’s call the function homepage() and put it in a file called homepage.js. Here’s how that file looks now:

Now we need to actually create the file so we can add this HTML to it. We can make that a function called addHomepage() and also add that to the same file. Here’s how it looks:

Now we can simply export it out using module.exports = addHomePage and call it in our index.js file. Here’s our revised index.js file:

As you can see, I also sorted the posts by latest date so that the latest post is first.

The Assets Directory

We can store any files that we don’t want to be touched by the generator in ./public/assets. For example, if you wanted to add styling to this blog, you could add the following to your homepage function:

<link rel=”stylesheet” href=”./assets/main.css” />

And now you can style your blog as you like. Images also work in the same way. For example, if in a post Markdown file you wanted to add an image, you could do the following:

Here’s an image:![Wow look at this beautiful thing](../assets/images/wow.png)

Making It Look Pretty

Ah! Now for my favorite part: It’s time to make it look nice. I don’t know about you, but looking at those default HTML styles was hurting me a lot. To make my life simple, I’m just going to plug grotesk into the project and customize it. Here’s the ./public/assets/styles/main.css file:

As you can see, I’ve decided to go with fluid type for this project. I also brought in grotesk.light.scss and customized the variables. Here’s how the variables look now:

I also customized the fonts.scss file that came with grotesk. Here’s how it looks like now:

As you can see, I’ve imported two fonts for this blog: Lyon Display (locally hosted) and EB Garamond (a Google Font).

That’s it for the styling. It ended up looking way better than I expected. You can check it out live, but if you can’t be bothered, here’s an image:

An image of the final result

Hosting

I personally like using ZEIT Now for hosting, but some other free options I like as well are Netlify and GitHub Pages. Since Now integrates so well with NPM build scripts that output to a public directory, all I had to do was run now — prod in the root of the directory (when you run it the first time, it’ll ask you some configuration questions to which the default answer is “fine”). Now every time I want to update my blog, all I have to run is now — prod again and it’ll update my blog by running npm run build and host it.

Final Thoughts

Thanks for reading this very long article. I hope you learned a little something about Node.js. I personally learned a lot about the fs API and enjoyed making this a lot. I liked it so much that I actually ended up switching my personal blog from Gatsby to this. That might be a bad decision, but I can always work it out later. Just a reminder: You can find all the source code on GitHub, so feel free to fork it or open an issue if you find something wrong.

I’m going to keep working on this to fine-tune it to my needs and maybe experiment with a few different things like lit-html or mustache for templating. But that’s it for this article. See you in another one very soon.

--

--