Static Web Site Generation (Part 3)

May 20, 2023

Previous posts in the series: Part 1, Part 2.

This is the latest installment of a series detailing how I maintain this website through a custom-built static web site generator I call webgen. Earlier posts described the general architecture of the tool, which is based on the idea of injecting HTML content files into templates to produce pages that can be statically served using a bog-standard file-based web server. (In the case of this website, Github Pages.)

What I'd like to talk about today is the support I coded into webgen for maintaining this blog. It builds directly onto the Markdown processing I described in my last post.

Rather than being abstract and general like I've been until now, though, let me show how this very blog is set up. The reason is less pedagogical than expedient: blog support in webgen is very much geared towards how I thought about setting up this blog in the first place and how I want to work on my posts. Let's just say that it's not the most generic tool I've ever written.

You can follow along at https://github.com/rpucella/rpucella.github.io. The blog is hosted under /blog/. In that folder, there's a webgen source folder .__src holding templates. The template for the website as a whole is /.__src/CONTENT.template. (Note that I'm using .__src instead of the __src folders that I described in earlier posts. The leading dot is optional in webgen, and I'm using it here because on Github Pages it prevents the folder from being served. Not that it would greatly matter if it were, but it's slightly cleaner.)

Source folder /blog/.__src/ contains a subfolder POSTS/ understood by webgen and holding the blog posts source. Each blog post is in its own subfolder in POSTS/. For instance, this post is in /blog/.__src/POSTS/static-web-site-generation-3/ and contains two files, the Markdown source document of the post index.md, and the image file sunset.jpg for this image:

Sunset

(Photo by Joshua Woroniecki on Unsplash)

The Markdown source document starts with metadata for the title and date of posting:

---
title: Static Web Site Generation (Part 3)
date: 2023-05-20
---

*Previous posts in the series: [Part 1](/blog/post/static-web-site-generation), 
[Part 2](/blog/post/static-web-site-generation-2)*.
...

Running webgen will find /blog/.__src/POSTS/, recognize it as a collection of blog posts, and generate both the posts content proper and a summary page. Let's see how that's done.

The posts content is generated by first creating a subfolder /blog/post/, and copying every subfolder in /blog/.__src/POSTS/ to /blog/post/. This means, in particular, that the subfolder name chosen in POSTS/ for the post becomes part of the route for serving the post on the website. The only adjustment made to the copied folder is that the source file index.md is moved to a source subfolder .__src/ of the copied folder.

Generating /blog/post/ is the first thing webgen does before all other generations. Thus, subsequent generation passes will translate all Markdown files into HTML content files, including those source index.md files that have been placed in the appropriate subfolders of /blog/post/. As usual with webgen, the generated index.content files will get converted to HTML files by injecting them into the website template. All in all, once /blog/post/ is created and __src/POSTS/ subfolders are copied, the rest of the process relies on how webgen works independently of any blog support.

Posts for this blog are formatted with the help of a template /blog/.__src/MARKDOWN.template that knows how to handle the title and date present in the metadata of the various index.md files:

<main>

  <article class="post">

    <h1 class="title">{{ .Title }}</h3>

    <div class="date">{{ .FormattedDate }}</div>

    <div class="body">
      {{ .Body }}
    </div>

    {{if .Reading}}
      <div class="reading">{{ .Reading }}</div>
    {{end}}

  </article>

</main>

Just like all other webgen templates, the syntax is simply that of Go's html/template package.

As part of the creation of /blog/post/, webgen also creates a summary page — basically a table of content for the blog, listing all the posts. The tool simply pulls all the metadata from all the posts it copies to /blog/post/ and collects them into /blog/.__src/index.content using a dedicated posts summary template /blog/.__src/SUMMARY.template:

<main>

  <h1>Close Encounters of the Logical Kind</h1>

  <p>Infrequent riffs about software development, computer science, and mathematics.</p>

  {{ range .Posts }}

    <article class="summary">
      <div class="title"><a href="/blog/post/{{ .Key }}">{{ .Title }}</a></div>
      <div class="date">{{ .FormattedDate }}</div>
    </article>

  {{ end }}

</main>

The resulting index.content file will get converted to a final HTML file by webgen in the usual way.

Full disclosure: I'm not entirely happy with how summarizing is done. Among other things, it requires a template SUMMARY.template, unlike elsewhere in webgen where templates are entirely optional. It also does not support pagination, so this won't scale if this blog ever gets large. I expect, however, that I'll have a sense of what a better approach is by then. Meanwhile, I'll live with the current design.

Aside from the summary page generation, I'm really happy about how blog support came out. It fits seamlessly with the cascaded generation philosophy: blog post generation merely lays out the structure of the blog posts hierarchy, and doesn't need to know anything about how the final content gets turned into final HTML.

This is obviously not a complete blog hosting solution. It lacks basic features such as tagging or previous/next post navigation. Those should be straightforward to add, though, when I have a bit of free time. Stay tuned.

The Ebony Tower (by John Fowles)