diff options
Diffstat (limited to 'src/blog/makefile-based-blogging/index.md')
-rw-r--r-- | src/blog/makefile-based-blogging/index.md | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/src/blog/makefile-based-blogging/index.md b/src/blog/makefile-based-blogging/index.md new file mode 100644 index 0000000..9244a0d --- /dev/null +++ b/src/blog/makefile-based-blogging/index.md @@ -0,0 +1,249 @@ +--- +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 +<html lang="en"> + <head> + <meta name="author" content="$author-meta$"> + <meta name="description" content="$description$"> + </head> + <body> + <h1 class="title">$title$</h1> +$body$ + </body> +</html> +``` + +[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. |