I wanted to create a lean, flexible, and efficient workflow using mostly text based tools to generating a static website hosted on GitHub Pages. When I began looking how other people handled their static websites, I saw many frameworks/programs out there that did basically exactly what I wanted to do – Hugo, Jekyll. Honestly, these just seemed too complex, and what is the fun in not building something yourself.
Markdown
It’s widely used and simple to learn. I wanted each file to only be concerned with it’s own specific content – separation of concerns. The presentation and the formatting on the page should be handled separately, so that the content or the presentation can change independently of each other. Markdown makes it easy to write content, more importantly easy to read. It also allows me to use a simple text editor neovim to write the content. And best of all, it’s easily converted to HTML (or PDF, etc).
Pandoc
Pandoc is awesome. The universal markup converter.
Pandoc is a Haskell library for converting from one markup format to another, and a command-line tool that uses this library.
It supports templating, and a wide range of formats. It’s the bridge from the Markdown files to final HTML.
Makefile
Coming from a background in C programming this felt like a trip back in time. Makefiles are efficient, language agnostic, and highly configurable for the exact needs of the project. You don’t have to make it complicated if you don’t want to.
Project Layout
I setup my project similar to how I setup my scala projects. I
created a src
folder for all my “source files” (markdown,
assets, templates, etc).
src
├── assets
│ ├── images
│ │ ├── logo.jpg
│ │ ├── favicon.ico
│ └── style
│ └── style.css
├── markdown
│ ├── index.md
│ ├── posts
│ │ ├── post-topic-1.md
│ │ ├── post-topic-2.md
│ │ ├── post-topic-3.md
│ │ ├── post-topic-4.md
│ │ ├── post-folder
│ │ │ ├── post-folder-topic-1.md
│ │ │ ├── post-folder-topic-2.md
│ │ │ ├── post-folder-topic-3.md
│ ├── posts.md
├── site.webmanifest
└── templates
├── _footer.html
├── _header.html
├── html.template
└── _navigation.html
I then have a target
directory in the root folder of
my project where the .md
files are converted to
.html
and served.
Makefile
The beauty of using a Makefile is that files that aren’t changed aren’t rebuilt. Additionally, you can force file to be rebuilt if it a dependency is has changed, but the file itself hasn’t.
For example:
- Run
make clean all
## removes the target and rebuilds all files. - Update
index.md
- Run
make
## onlyindex.html
is created.
If I make updating a template a dependency to the markdown files, then when those are updated, any files that depend on them will be rebuilt.
- Run
make clean all
## removes the target and rebuilds all files. - Update
templates/_header.html
- Run
make
## all markdown files are rebuilt that depend on that header file.
Let’s first start off by setting up some variables:
# Variables
SHELL := /bin/bash
SOURCE_DIR := src
MARKDOWN_DIR := $(SOURCE_DIR)/markdown
ASSETS_DIR := $(SOURCE_DIR)/assets
TEMPLATES_DIR := $(SOURCE_DIR)/templates
TARGET_DIR := target
DEPLOY_DIR := docs
These variables mimic my project layout.
Next let’s define more variables that represent our source files.
# Find all markdown files
MARKDOWN_FILES := $(shell find $(MARKDOWN_DIR) -type f -name '*.md')
# Generate target HTML files paths
TARGET_HTML_FILES := $(patsubst $(MARKDOWN_DIR)/%.md,$(TARGET_DIR)/%.html,$(MARKDOWN_FILES))
# Find all assets
ASSETS_FILES:= $(shell find $(ASSETS_DIR) -type f)
# Generate target image files paths
TARGET_ASSETS_FILES := $(patsubst $(SOURCE_DIR)/%,$(TARGET_DIR)/%,$(ASSETS_FILES))
# Find all template files
TEMPLATE_FILES := $(shell find $(TEMPLATES_DIR) -type f)
The TARGET_HTML_FILES
and the
TARGET_ASSETS_FILES
will represent our dependencies for
our main project.
Next, is the pandoc command to use a stylesheet, HTML template, a header, a footer, and a naviation to convert our Markdown files to HTML.
# Pandoc command
PANDOC := pandoc --from=markdown --to=html \
--template=$(TEMPLATES_DIR)/html.template \
--css=/assets/style/style.css \
--include-in-header=$(TEMPLATES_DIR)/_header.html \
--include-after-body=$(TEMPLATES_DIR)/_footer.html \
--include-before-body=$(TEMPLATES_DIR)/_navigation.html \
--standalone
Next comes the main build with the target assets and HTML files.
# Default target
all: $(TARGET_ASSETS_FILES) $(TARGET_HTML_FILES)
.PHONY: all
This Makefile rule looks for any dependencies in the target
directly that end in .html
. These have 3 dependencies,
the original markdown files, the template files, and the asset files.
The | $(TARGET_DIR)
is a pre-requisite for the
dependencies. The directory in the target is created, and the pandoc
command above is run.
# Rule to create HTML files, make sure the target directory is created first.
$(TARGET_DIR)/%.html: $(MARKDOWN_DIR)/%.md $(TEMPLATE_FILES) $(ASSETS_FILES) | $(TARGET_DIR)
@# Create the subdirectory if needed
@echo $<
@mkdir -p $(@D)
@$(PANDOC) -o $@ $<
This Makefile rule looks for any dependencies in the target assets
directory. There’s a single dependency that matches any file in the
source assests directory. Again, the TARGET_DIR
is a
pre-requisite. The directory is created, and the file is copied.
# Rule to copy assets files, make sure the target directory is created first.
$(TARGET_DIR)/assets/%: $(ASSETS_DIR)/% | $(TARGET_DIR)
@# Create the subdirectory if needed
@mkdir -p $(@D)
@cp $< $@
Finally the TARGET_DIR
rule. This ensures the
directory is created
# Create target directory
$(TARGET_DIR):
mkdir -p $@
Additionally, I added some helpful rules to run
,
deploy
, info
, and clean
.
deploy:
@# Remove
@rm -fr $(DEPLOY_DIR)
@mkdir -p $(DEPLOY_DIR)
@cp -r $(TARGET_DIR)/* $(DEPLOY_DIR)
.PHONY: deploy
# Help command
help:
@echo "Available commands:"
@echo " make all - Build the entire project (default)"
@echo " make clean - Remove the target directory"
@echo " make help - Display this help message"
.PHONY: help
info:
@echo $(SOURCE_DIR)
@echo MARKDOWN_FILES=$(MARKDOWN_FILES)
@echo TARGET_HTML_FILES=$(TARGET_HTML_FILES)
@echo TEMPLATE_FILES=$(TEMPLATE_FILES)
@echo ASSETS_FILES=$(ASSETS_FILES)
@echo TARGET_ASSETS_FILES=$(TARGET_ASSETS_FILES)
.PHONY: info
clean:
@echo "cleaning project"
@rm -rf $(TARGET_DIR)
.PHONY: clean
2024-Aug-25