← Back

How I generate my static site with Node.js and Pug.

I decided to write my own minimal Static Site Generator for this Neocities site rather than use an established system like Hugo or Gatsby (Although, as a React supremacist, I was tempted by Gatsby).

Both options mentioned are robust and perfectly capable, but were a bit bulky for my needs. Considering my current state of having too much time on my hands, I decided to jump on the chance to learn something new - I've never used or written a SSG before.

Javascript is the programming language that I'm most comfortable with, so I chose to write my SSG engine via Node.

This site houses a blog that's frequently updated, and I needed a way to generate HTML pages from markdown files to avoid laboring over hand-written HTML for each post. I decided to use Showdown to handle the parsing. It's intuitive, and requires just two lines of code to get the ball rolling.


To avoid copy pasting chunks of markup throughout my pages, I utilize a templating language for Node called Pug. Being a javascript junkie, I had initially accomplished this templating using JS template literals. While it served its purpose, long template literals can be cumbersome to manage, and I quickly acquired a headache updating them. Pug, looking a bit like a twisted love child of Python and HTML, offers a much cleaner solution. It also allows for inline JS execution, which is quite handy.

Chunks of Pug can be plugged into other Pug files for reuse via a system called "mixins". This feature is particularly useful for my Navbar, which renders on nearly every page of my site.

mixin navWithHeader

h1 Sleepy dev πŸŒ™

        a(href="index.html") 🏠 Home
        a(href="about.html") 🧸 About
        a(href="thoughts.html") πŸ’­ Blog
        a(href="microblog.html") πŸ› Microblog
        a(href="misc.html") πŸ—ƒ Misc.
        a(href="guestbook.html") πŸ““ Guestbook

Below is an example of a Pug file that I use to generate my thoughts.html page, which renders a list of all of my blog posts in reverse chronological order.

include  nav.pug

doctype html
link(rel="stylesheet"  href="index.css"  type="text/css")
link(rel="icon"  type="image/x-icon"  href="assets/favicon.png")


- const  sortedPosts = postData.sort((x, y) =>  new  Date(y.pubDate) - new  Date(x.pubDate))
        each  post  in  sortedPosts
                a(href=`posts/${encodeURIComponent(post.title)}.html`)= post.title

a(href="feed.xml"  target="_blank") πŸ’₯ Subscribe via RSS

The (relevant) file structure of my SSG looks something like this: src/ houses my generation logic and layout files, posts/ holds my blog post markdown files, and site/ is where the generated static site files live. site/ is what's uploaded to Neocities.

β”œβ”€ thoughts.pug
β”œβ”€ postMetaData.json
β”œβ”€ post.pug
β”œβ”€ index.pug
β”œβ”€ nav.pug
β”œβ”€ index.js
β”œβ”€ rss.js
β”œβ”€ blogPost.md
β”œβ”€ assets/
β”œβ”€ posts/
β”‚  β”œβ”€blogPost.html
β”œβ”€ index.html
β”œβ”€ index.css
β”œβ”€ feed.xml

Generating files

The following JS block loops over each markdown file I have in my /posts folder, converts each to a string of HTML , and passes that HTML chunk into a Pug render function to be consumed by a Pug template file that wraps the chunk in layout code. The final HTML string is written to a .html file in the /site folder.

const  converter = new  showdown.Converter();

fs.readdirSync("./posts/").forEach((filePath) => {
    const  postHtml = converter.makeHtml(fs.readFileSync(`./posts/${filePath}`, 'utf8'));
    const  pageHtml = pug.renderFile('src/post.pug', { postHtml:  postHtml });
    const  fileName = filePath.split('.')[0];
    fs.writeFileSync(`site/posts/${fileName}.html`, pageHtml);

Post "metadata" is generated automatically with each build. New articles are assigned a publication date of the current date. I could also assign a publication date to each article manually within the markdown, but this solution is working well enough for now.

postFilePaths.forEach((file) => {
    const  fileName = file.split('.')[0];
    const  postMetaDataExists = postMetaData.find((data) =>  data.title === fileName)
    if (!postMetaDataExists) {
        postMetaData.push({ title:  fileName, pubDate:  new  Date() });
    fs.writeFileSync(`src/postData.json`, JSON.stringify(postMetaData));

My blog features an rss feed, which is generated with the help of an rss Node package. In hindsight, my feed is simple enough that I could forgo usage of this library; but it's working well and I haven't felt compelled to roll my own solution.

const  writeXMLFeed = () => {
    const  postMetaData = JSON.parse(fs.readFileSync('src/postMetaData.json'));
    const  converter = new  showdown.Converter();

    const  feed = new  RSS({
        title:  "SleepyDev's Blog",
        description:  'SleepyDev Blog Feed',
        feed_url:  'https://sleepydev.neocities.org/rss.xml',
        site_url:  'https://sleepydev.neocities.org',
        webMaster:  'SleepyDev',
        copyright:  '2023 SleepyDev',
        language:  'en',
        ttl:  '60',

    postMetaData.forEach((post) => {
        const  postContent = fs.readFileSync(`posts/${post.title}.md`, 'utf8');
        const  contentHTML = converter.makeHtml(postContent);
        //strip out the header to avoid RSS readers repeating it
        const  contentWithoutHeader = contentHTML.split('</h2>')[1];

            title:  post.title,
            description:  contentWithoutHeader,
            url:  `https://sleepydev.neocities.org/posts/${encodeURIComponent(post.title)}.html`,
            date:  post.pubDate,

    fs.writeFileSync(`site/feed.xml`, feed.xml());

While it's a little hacky (my speciality) and clearly not well optimized, the setup has been treating me well. This generation pattern also extends to my microblog, booklog, and short fiction journal. Knowing that this automation will take care of most of the heavy lifting for the content heavy sections of my site encourages me to spend more time focusing on writing and less time grinding out hardcoded markup. πŸ’«