Automating the Little Things
I started working on this site only a few weeks ago, and there was a bunch of manual work involved at that time. When I wrote Creating PDFs from HTML Using C#, I didn't have a design. I wrote the article's content in Markdown and, once I had a rough draft, created the HTML for it by hand. I then used that as a base to create a design and vowed I would never do that copy/pasting dance ever again.
After drafting an article in Markdown, including its metadata (title, published date, tags, and so on), the manual steps to releasing it broadly looked like this:
- Convert the Markdown to HTML
- Zip up all the files
- Deploy the zip file
But that first step involved doing a number of smaller things, such as adding the metadata to the HTML, ensuring external links open in new tabs, and making sure all articles have identical HTML structure for styling, to name a few. None of it is difficult, but it adds friction to the writing process, and encourages making mistakes.
This is what I wanted instead:
- Extract the metadata and generate HTML from the content in the way I like, so I don't do it by hand
- Inject that metadata and generated HTML into templates, so the HTML structure across the site is guaranteed to be the same
- Write the results of the previous step to individual
.htmlfiles, where thetitledefined in an.mdfile dictates the name of the.htmlfile - Replace any internal links (i.e. links to my own articles) found in
.mdfiles with links to the corresponding.htmlfiles - Take the list of generated articles and add links to them in
index.html - Zip up all the files
- Deploy the zip file
Fast-forward a couple of weeks and the first six of those are now automated. If you're interested in any of the Markdown to HTML parts, I wrote A Crash Course in Markdig (notice this is an internal link!) to share what I learned from building that functionality with Markdig.
For the templating, I used Razor.Templating.Core, which is a wrapper around the ASP.NET Razor templating engine that allows it to be used in non-ASP.NET contexts. I have a lot of experience working with Razor so it was natural to want to use it in my personal projects.
Handling internal links
One thing I didn't cover in the Markdig article is how I approached rewriting internal links. Let's say I have these two articles:
---
title: My First Article
---
Some article text.
---
title: My Second Article
---
Some article text.
Let's also assume they're stored in one.md and two.md, respectively. If I want to add a link from one.md to two.md, I can do that like so:
[internal:A link to two](two.md)
The tricky part here is that, at parsing time, I don't know what two.md's corresponding .html file name will be (recall the .html file name is based on the title specified in the .md file). That means internal links like this can't be added during the Markdown parsing phase. Instead, they need to be post-processed.
To accomplish this, I wrote a Markdig extension which keeps track of all internal links found within a file. Once all articles have been parsed, I'm left with a Dictionary<string, Article>, where string is the name of a Markdown file (e.g. one.md), and Article is as follows:
public class Article
{
[YamlMember(Alias = "title")]
public required string Title { get; set; }
// ... other properties
[YamlIgnore]
public string? FileName { get; set; }
[YamlIgnore]
public Dictionary<string, List<string>> InternalLinks { get; set; } = [];
}
This means I can use the name of a Markdown file to lookup its article, giving me access to its FileName and collection of internal links. Like the previous dictionary, the string for InternalLinks represents the name of a .md file, which is the file being linked to. The List<string> is then the collection of link text specified. This enables multiple internal links from one.md to two.md with different link text:
---
title: My First Article
---
Some article text and a link to [internal:two](two.md).
Later on, I add a second link to two but with [internal:different link text](two.md).
The post-processing stage goes through all of the internal links in the relevant HTML files and replaces them with the correct text and .html file link. No manual work required.
What about zip deployments?
I partly tackled this today with some PowerShell:
# build and publish all articles
& "$PSScriptRoot\build.ps1"
$publishDirectory = "published"
$releasesDirectory = "releases"
if (-Not (Test-Path $publishDirectory)) {
Write-Host "The publish directory does not exist."
exit 1
}
if (-Not (Test-Path $releasesDirectory)) {
Write-Host "The release directory does not exist."
exit 1
}
# create the zip file path and name
$timestamp = Get-Date -Format "yyyy-MM-dd-HHmm"
$zipFileName = "$timestamp.zip"
$zipFilePath =
Join-Path -Path (Get-Location) -ChildPath $releasesDirectory |
Join-Path -ChildPath $zipFileName
# create the zip file and write it to the destination directory
Write-Host -ForegroundColor Cyan "Creating zip file: $zipFilePath"
Compress-Archive -Path "$publishDirectory\*" -DestinationPath $zipFilePath -Force
Write-Host -ForegroundColor Green "Zip file successfully created."
Write-Host ""
This is largely self-explanatory. The build.ps1 script builds my blog engine and runs it to make sure all articles are up-to-date. Once that script has finished, the current script takes everything in the published directory, zips it up and writes a zip file to the releases directory, leaving me with a file name like 2025-03-04-1218.zip.
The next step will be to look at using Cloudflare's wrangler tool to create deployments. I expect this to replace my zip file script, since it can be configured to deploy the contents of a directory. It also handles bundling, which is something I've not sorted out yet. But I wrote the PowerShell script in the interests of time, and I'm happy with that for now.