Writing my own static site generator
Making a Simple Static Site Generator
In my last blog entry I talked about the difficulties in maintaining a growing blog using just HTML and CSS. I got tired of it immediately after I posted that blog. In this entry I will discuss the next steps I took and why I did what I did.
The Pain Points - A Recap
As I mentioned last time, as the list of blog entries grows, it becomes harder and harder to keep every page in sync with each new entry. As of the last entry, it was four distinct pages that needed to be updated each time. At my current rate of one to two blog posts a week, I would be touching a hundred files for every update within a year. That isn't sustainable.
I have several options here, I can write a dumb Powershell script to go through and update each and every page's nav bar, I could generate stubbed HTML and use either a C# or Powershell script to update the headers, or I could go with a static site generator. Based on the title of the page, I think you can guess what I decided to do.
Designing an MVP
I could start writing a static site generator and continue for weeks and weeks. As such, I gave myself one morning to figure it out. The best place to start here is to identify what features were actually necessary, what would be nice to have, and what could wait for later. The necessary features I decided on are:
- Take some written format and output blog posts in plain HTML and CSS.
- Have a configurable Title.
- Allow for templating.
- Fit into an Azure Devops build pipeline so there would be minimal changes necessary for deployment.
Now, there are some unanswered questions here. What does some written format entail? How should a Title be configurable? How should I allow for Templating? Let's tackle each one of these individually.
Some Written Format
I needed a format that would be robust enough to allow for me to just Write. My options are fairly straight forward, HTML or Markdown. By writing HTML stubs, I get huge control over what the blog looks like. I could throw in weird custom styling or whatever my twisted little heart desires. Markdown is more limited, there are parsers out there and converters that can take markdown and generate HTML but there would be some amount of conversion or processing I would need to do.
I decided to go with Markdown. Why? Because it is reasonably full-featured and there are a ton of libraries out there to handle it. I landed on Markdig. It's extensible, easy to use, and is open source.
Configurable Title
Now that I had a choice for my Written Format and my library for parsing, I could see what Markdig supports. For instance, it supports YAML Front Matter. YAML front matter is a very standard approach for Site Generators that use Markdown documents. You put information into a block at the beginning of the document and it can parse it and come up with information necessary for this particular entry.
So now my markdown documents have a YAML blob at the start. Since it is just a blog, I can add more information as time goes on. For instance, I have the publish dates saved, so I could update the header or footer of the page to include this information.
Allow For Templating
How should I allow for Templating? The easiest approach that I could think of at the moment was to generate a stubbed HTML file that included all the static material. Things like the style sheet, my footer information, and so on.
The Implementation
I then sat down and started working on building out the actual CLI tool. I knew I would have BlogPosts that themselves had Title and Content. The Content would come in two stages, the raw markdown and the converted HTML. I do some things with semantic HTML that I had to account for with my Blog Posts, for instance I have <section> blocks surrounding logical sections. It essentially ties together a section by pulling all the content between each h1 and h2 element and calling that a section. At least, that is the convention that I was using.
Beyond that, there was the rest of the site that needed to be copied to an output directory. Not to mention that static index.html page needed to have it's navigation updated for each new blog post. I decided to use System.Xml.Linq to parse the HTML and provide an easy to use interface to rewrite the HTML. One thing I forgot though is that HTML is not valid XML as far as System.Xml.Linq is concerned.
The Domain
Anyone who has worked with me since about 2019 knows that I really dug in on the concepts of Domain Driven Design. I applied those concepts to this application as well. I have three basic page types; Blog Page, Blog Post, and Tag Page. The Tag page is new. I figured, why not add that while I can. Beyond that, there is the aggregate of the Blog Site as a whole. The Blog Site knows how to Generate the file structure and knows how to get the generated Content for each page type. It knows that there will be an Index page, Contact page, and a number of Blog pages and that it must generate Tag pages based on the Blog pages. The Blog Page knows how to convert the Markdown into valid HTML and how to apply the special <section> templating that I mentioned above. The other page types extend the Blog Page. The Blog Post overrides the Special Formatting to apply the new Post Header that includes tag links.
Unit Testing
I really wanted to try something new with the unit testing here, I wanted to lean into BDD or Behavior Driven Development. Essentially, I wanted to write Gerkhin style requirements and have the tests use those requirements to perform the tests. In comes Reqnroll. This is a very feature rich library that converts Gerkhin into actual test code. I was able to write a single file with five Scenarios that used only three unique functions to test all sorts of different scenarios. Below is what one of my test Scenarios looks like.
Scenario: Convert Markdown to HTML
Given I have the following Markdown:
"""
# Hello World
This is a test.
"""
When I convert the Markdown to HTML
Then the result should be:
"""
<article>
<section id="hello-world">
<h1>Hello World</h1>
<p>This is a test.</p>
</section>
</article>
"""
The Result
I now have a fully functional static site generator! Well, fully functional enough for me. Again, I won't win any awards for this bare bones implementation. I did, however, have a good amount of fun and solve a real issue that I was running into. This gives me a solid foundation to continue building this blog, even as I find things to nitpick and improve.