--- title: Makefile-Based Blogging date: December 12, 2022 subtitle: Yet another static site generator using `pandoc(1)` and `make(1)`. description: Building a markdown-based static site generator using pandoc and make. --- A few days ago, I got the gumption to start blogging again. The last time I wrote with any frequency, I lovingly hand-crafted each HTML file before `rsync`ing it to my web server. This time, I wanted a more efficient workflow. I surveyed the [vast number](https://github.com/myles/awesome-static-generators) of static site generators available on GitHub, but most of them seemed like overkill for my humble website. I figured that by the time I wrapped by head around one of them, I could have just written a Makefile. Finally, I came across [pandoc-blog](https://github.com/lukasschwab/pandoc-blog), which gave me inspiration and showed me the ideal pandoc incantations for generating HTML from markdown files. And thus, my [Makefile-based static site generator](https://git.sacredheartsc.com/www/about/) was born. You're reading the inaugural post! ## Generating the HTML The workhorse of this thing is [pandoc](https://pandoc.org), which is a ubiquitous open-source document converter. Transforming markdown into HTML is as simple as: ```bash pandoc document.md -o document.html ``` Simple! But to generate an entire website, we'll need some of pandoc's additional features: custom templates and document metadata. ### Custom Templates The layout of pandoc's output document is determined by the [template](https://pandoc.org/MANUAL.html#templates) in use. Pandoc includes default templates for a variety of document formats, but you can also specify your own. A very simple HTML template might look something like this: ```html

$title$

$body$ ``` [My pandoc template](https://git.sacredheartsc.com/www/tree/templates/default.html) is what generates the navigation bar at the top of this page. The variable `$body$` is replaced by the content of your markdown document when pandoc renders the template. The other variables are replaced by their corresponding values from the document's metadata. ### Document Metadata Each pandoc source document can have associated metadata values. There are three ways of specifying metadata: the `--medatata` [flag](https://pandoc.org/MANUAL.html#option--metadata), a dedicated [metadata file](https://pandoc.org/MANUAL.html#option--metadata-file), or a [YAML metadata block](https://pandoc.org/MANUAL.html#extension-yaml_metadata_block) embedded within the document itself. We'll be using the embedded metadata blocks. Each markdown document for my website starts with a YAML metadata block. The metadata for the post you're [currently reading](https://git.sacredheartsc.com/www/tree/src/blog/makefile-based-blogging/index.md) looks like this: ```yaml --- title: Makefile-Based Blogging date: December 12, 2022 subtitle: Yet another static site generator using `pandoc(1)` and `make(1)`. description: Building a markdown-based static site generator using pandoc and make. --- ``` You can put whatever YAML you like in your markdown files, as long as the metadata starts and ends with three hyphens. ## Automating pandoc with make Using a Makefile, we can automatically invoke pandoc to convert each markdown file in our blog to HTML. In addition, `make` will keep track of which source files have changed since the last run and rebuild them accordingly. First, lets describe the project layout: - **src/**: the source files of our blog, including markdown files and static assets (CSS, images, etc). The subdirectory structure is entirely up to you. - **public/**: the output directory. After running `make`, the contents of this directory can be `rsync`'d straight to your web server. - **scripts/**: helper scripts for generating the blog artifacts. Currently there are only two: - [bloglist.py](https://git.sacredheartsc.com/www/tree/scripts/bloglist.py) generates a markdown-formatted list of all your blog posts, sorted by the `date` field in the YAML metadata block. - [rss.py](https://git.sacredheartsc.com/www/tree/scripts/rss.py) generates an RSS feed for your blog. - **templates/**: pandoc templates which generate HTML from markdown files (currently, there is only one). The Makefile used to build this website is located [here](https://git.sacredheartsc.com/www/tree/Makefile). I've reproduced a simplified version below, to make it easier to step through. ```makefile ###################### # Variable definitions ###################### # These variables are used to generate the RSS feed URL = https://www.sacredheartsc.com FEED_TITLE = sacredheartsc blog FEED_DESCRIPTION = Carolina-grown articles about self-hosting, privacy, unix, and more. # The number of blog posts to show on the homepage BLOG_LIST_LIMIT = 5 # File extensions (other than .md) that should be included in public/ directory STATIC_REGEX = .*\.(html|css|jpg|jpeg|png|xml|txt) # Pandoc template used to generate HTML TEMPLATE = templates/default.html # List of subdirectories to create SOURCE_DIRS := $(shell find src -mindepth 1 -type d) # List of source markdown files SOURCE_MARKDOWN := $(shell find src -type f -name '*.md' -and ! -name .bloglist.md) # List of static assets SOURCE_STATIC := $(shell find src \ -type f \ -regextype posix-extended \ -iregex '$(STATIC_REGEX)') # List of all blog posts (excluding the main blog page) BLOG_POSTS := $(shell find src/blog \ -type f \ -name '*.md' \ -and ! -name .bloglist.md \ -and ! -path src/blog/index.md) # Subdirectories to create under public/ OUTPUT_DIRS := $(patsubst src/%, public/%, $(SOURCE_DIRS)) # .html files under public/, corresponding to each .md file under src/ OUTPUT_MARKDOWN := $(patsubst src/%, public/%, $(patsubst %.md, %.html, $(SOURCE_MARKDOWN))) # Static file targets under public/ OUTPUT_STATIC := $(patsubst src/%, public/%, $(SOURCE_STATIC)) # Script to generate RSS feed RSSGEN = scripts/rss.py \ src/blog \ --title="$(FEED_TITLE)" \ --description="$(FEED_DESCRIPTION)" \ --url=$(URL) \ --blog-path=/blog \ --feed-path=/blog/rss/feed.xml ###################### # File Targets ###################### # Default target: convert .md to .html, copy static assets, and generate RSS public: \ $(OUTPUT_DIRS) \ $(OUTPUT_MARKDOWN) \ $(OUTPUT_STATIC) \ public/blog/feed.xml # Homepage (/) public/index.html: src/index.md src/.bloglist.md $(TEMPLATE) sed $$'/__BLOG_LIST__/{r src/.bloglist.md\nd}' $< \ | pandoc --template=$(TEMPLATE) --output=$@ # Markdown list of 5 most recent blog posts src/.bloglist.md: $(BLOG_POSTS) scripts/bloglist.py scripts/bloglist.py src/blog $(BLOG_LIST_LIMIT) > $@ # The main blog listing (/blog/) public/blog/index.html: src/blog/index.md src/blog/.bloglist.md $(TEMPLATE) sed $$'/__BLOG_LIST__/{r src/blog/.bloglist.md\nd}' $< \ | pandoc --template=$(TEMPLATE) --output=$@ # Markdown list of _all_ blog posts src/blog/.bloglist.md: $(BLOG_POSTS) scripts/bloglist.py scripts/bloglist.py src/blog > $@ # Convert all other .md files to .html public/%.html: src/%.md $(TEMPLATE) pandoc --template=$(TEMPLATE) --output=$@ $< # Catch-all: copy static assets in src/ to public/ public/%: src/% cp --preserve=timestamps $< $@ # RSS feed public/blog/feed.xml: $(BLOG_POSTS) scripts/rss.py $(RSSGEN) > $@ ###################### # Phony Targets ###################### .PHONY: serve rsync clean # Run a local HTTP server in the output directory serve: public cd public && python3 -m http.server # Deploy the site to your webserver rsync: public rsync -rlphv --delete public/ webserver.example.com:/var/www/html clean: rm -rf public rm -f src/.bloglist.md rm -f src/blog/.bloglist.md ``` ## Closing Thoughts I admit, there is a small amount of hackery involved. You obviously can't generate a time-sorted list of blog posts using pure markdown, so I'm generating the markdown list using a Python script in an intermediate step. I then (ab)use `sed` to shove that list into the markdown source on the fly. This means that changing the look of the [blog list](/blog/) requires hacking up the Python code. But overall, I've been quite happy with this little project. There's just something about writing paragraphs in `vi` and typing `:!make` that warms my soul with memories of simpler times.