A Lazy Static Site Generator with Pandoc

2026-02-26

I hate blogging. It's not that I mind typing up my thoughts and what I've learned in order to share them with an audience that is anonymous at best and probably nonexistent. In fact, this is the part I enjoy; it's the only part of the blogging process that I want to deal with.

For me, potential blog posts are often a byproduct of reviewing and fleshing out a bunch of interconnected Markdown notes that I keep in a local folder. Once I am more or less happy with a text, I want it to be online immediately. But with the tools that I have used in the past (Wordpress, Jekyll, Hugo, Quartz) this always required some extra steps, like copying files to some specific folder, making sure that I use only the supported markup and committing and pushing to a git repository. All of this I found somewhat tedious. Plus, the tools that I used were all highly customizable, whether it was the layout or some additional plugins and features. For me they became a constant source of distraction and I spend my time tinkering with these tools instead of actually blogging.

So I was thinking: What could possibly be the minimal toolset for bringing a Markdown text online as a static website? Does one actually need more that pandoc, git and some shell scripting? I figured that I had to build this toolset myself, duct-taping existing tools together.1

Converting from Markdown to HTML

As I said, I like to write my notes in Markdown, but browsers prefer their good old HTML which pandoc, the awesome multitool of document conversion, can provide:2

$mdInputFile = "./A Lazy Man's Blog.md"

# slugify output file name to: a-lazy-man-s-blog.html
$htmlOutputFile = ((Get-Item $mdInputFile).BaseName -replace '[^\w]', '-').toLower() + '.html' 
pandoc $mdInputFile `
    --from=markdown `
    --to=html `
    --standalone `
    --toc `
    --output $htmlOutputFile

Pandoc creates nice, quite minimalist HTML files using its default templates. As I made a vo0-w to minimize my tinkering, I will be fine with pandoc's actually very sensible choices of default styling, which I think look great on both mobile devices and large screens.3

Gathering the source files

I like to keep my potential blog posts with my other notes. Actually, I don't even want to draw a clear distinction between a blog post and a mere note. Instead, I like to think of it as some of my notes are published and some are not.

If I want to publish a note, I add the following property to its YAML frontmatter:

---
publish: true
---

Now I can send a tool like ripgrep to rip through my heap of notes and come back to me with a list of file paths of all published or to-be-published notes:

rg -l "publish: *true" $PathWhereMyNotesLive

I then feed these links into pandoc and have it create HTML files in a single output folder.

Creating a date sorted list of blog posts

Traditionally, a proper blog should have a page listing all posts by date. For such a list I would never the following information from each blog post:

Date and title can be put into the YAML frontmatter of the note. The file link is a special case that I will deal with later. For now the question is, how to get the YAML frontmatter out of each note that is being converted into a blog post?

Extracting the YAML frontmatter of a note

We need some kind of extractor. The glorious yq just happens to have that functionality built in:

yq $mdInputFile --front-matter=extract -o json

This command returns a json representation of the file's front matter, which we can process further to create the list of posts.

function Get-BlogPostMetadata {
    param(
        [Parameter(ValueFromPipeline)]
        [string]
        $Path
    )
    Begin {
        $metadata = @()
    }

    Process {
    
        $metadata += yq --front-matter=extract $Path -o json | Out-String
    }
    End {
        '[' + ($metadata -join ',') + ']' | ConvertFrom-Json
    }
}

The resulting object can be used to populate a pandoc template.

Populating a pandoc template

Pandoc ships with a templating system that allows to use values from the YAML front matter. Let's create a new template for the post list, based on pandoc's default HTML template.

mkdir -p ~/.pandoc/templates
pandoc --print-default-template=HTML --output ~/.pandoc/templates/blogPostList.html

Add the following somewhere in the body section of the template:

<ul>
$for(posts)$
    <li><a href="$it.link$">[$it.date$] $it.title$</a></li>
$endfor$
</ul>

This will take the value of the metadata field posts and iterate over its content. The content is assumed to be an array of objects that have the properties link, date and title. Now we somehow need to provide this metadata to the blogPostList.html template.

With the help of our new Get-BlogPostMetadata function, we can create just the right input for the template.

$metadata['posts'] = Get-ChildItem $blogPostPaths | Get-BlogPostMetadata
$frontmatter = "---`n" + ($metadata | ConvertTo-Json -Depth 3 | yq -p=json -o yaml | Out-String ) + "---`n"

Unfortunately,.at least at the time of writing, there is no built in way that I know of to get from Powershell objects to YAML, so we have to convert to JSON first and then use yq And a little string manipulation to create an input that our pandoc template can process.

$frontmatter | pandoc --from=markdown --metadata title="Blog" --template=blogPostList

Bringing the HTML online

There are plenty of options of hosting a static website and while I was happy with github pages in the past, I am moving over to codeberg pages this time. Codeberg is a FOSS alternative to github and their infrastructure is fueled by donations. So "building" a static site blog in a CI pipeline, as it is common practice on github, sounds like a waste of codeberg's resources to me. I will, therefore, only commit the final HTML files.

In order to build and publish the latest version of all my blog posts I now merely need to run the following script:

#!/usr/bin/env pwsh
. ~/git/lazyblog/src/functions.ps1

Write-Host "Gathering posts to publish..."
$posts = Get-BlogPostsToPublish -Path '~/Obsidian/notes'

Write-Host "Building html posts..."
$posts | New-BlogPostBuild -OutputDir '~/git/manuel.batsching.cloud'

Write-Host "Building post list..."
$posts | New-BlogPostListBuild | Out-File '~/git/manuel.batsching.cloud/index.html'

Write-Host "Publishing all changes..."
Push-Location '~/git/manuel.batsching.cloud'
git commit -a -m 'Lazy blog update'
git push
Pop-Location
Write-Host "And Bam!"

As you can see, the script uses several PowerShell functions that are loaded (dot sourced) from a file called functions.ps1. To see, how exactly these functions implement the approach discussed above, have a look at: https://codeberg.org/manualbashing/lazyblog

Footnotes


[📧 Let me hear your thoughts on this post] [👆 Back to top] [🌐 Back to homepage]