Building a Static Site with Make
In this post, I’ll explain how this blog is built using just a Makefile. Static site generators like Jekyll, Hugo, or Gatsby are powerful, but sometimes you don’t need all that complexity. For most static content they can be quite the overkill. And when I tried building my Jekyll blog after some years of not updating it I encountered a lot of versioning and tooling errors that I want to avoid. Hell, even plain html files should be good enough right? So why not keep the functionality of writing content in Markdown but converting it into plain html using a Makefile?
With just:
- Make - for build automation
- Pandoc - for Markdown to HTML conversion
- Basic shell tools - sed, grep, find
You can create a fully functional blog with just these basic tools.
The project directory structure will look like this:
.
├── Makefile # Build configuration
├── src/
│ ├── content/ # Your blog posts (*.md files)
│ ├── templates/ # HTML templates
│ │ ├── header.html
│ │ └── footer.html
│ └── css/ # Stylesheets
│ └── style.css
└── build/ # Generated site (for publishing)
Each .md file in the content directory gets converted to
HTML and wrapped with header/footer templates. The index page is
automatically generated by scanning all markdown files and extracting
their titles and dates. CSS and other static assets are copied to the
build directory.
The section building the Markdown files look like this and basically just concatenate the header, converted Markdown content and footer together:
# Build individual blog posts
$(BUILD_DIR)/%.html: $(CONTENT_DIR)/%.md $(TEMPLATES_DIR)/header.html $(TEMPLATES_DIR)/footer.html $(TEMPLATES_DIR)/post.template
@mkdir -p $(dir $@)
@echo "Building $@..."
@cat $(TEMPLATES_DIR)/header.html > $@
@pandoc $< -f markdown -t html --template=$(TEMPLATES_DIR)/post.template >> $@
@cat $(TEMPLATES_DIR)/footer.html >> $@
The part building the homepage with an index of all blog pages is a bit more complicated (and hackisch you might say). It uses grep to find the title and date entry in the Markdown files. It stores these values in variables that can then be used to built html using string output (echo) which gets outputted to the final file.
# Generate index page with list of all posts
$(BUILD_DIR)/index.html: $(MARKDOWN_FILES) $(TEMPLATES_DIR)/header.html $(TEMPLATES_DIR)/footer.html
@echo "Generating index..."
@cat $(TEMPLATES_DIR)/header.html > $@
@echo '<div class="post-list">' >> $@
#@echo '<h1>Blog Posts</h1>' >> $@
@for file in $(MARKDOWN_FILES); do \
date=$$(grep -m 1 '^date:' $$file | sed 's/^date: *//' || echo "1970-01-01"); \
echo "$$date|$$file"; \
done | sort -r | while IFS='|' read date file; do \
title=$$(grep -m1 '^title:' $$file | sed 's/^title: *//' | sed 's/^"\(.*\)"$$/\1/'); \
[ -z "$$title" ] && title=$$(head -n1 $$file | sed 's/^# //'); \
link=$$(echo $$file | sed 's|$(CONTENT_DIR)/||' | sed 's/\.md/.html/'); \
display_date=$$(grep -m 1 '^date:' $$file | sed 's/^date: *//' || echo "No date"); \
categories=$$(grep -m 1 '^categories:' $$file | sed 's/^categories: *//'); \
echo "<div class='post-item'>" >> $@; \
echo "<h2><a href='$$link'>$$title</a></h2>" >> $@; \
echo "<p class='post-date'>$$display_date</p>" >> $@; \
#[ -n "$$categories" ] && echo "<p class='post-categories'>$$categories</p>" >> $@; \
echo "</div>" >> $@; \
done
@echo '</div>' >> $@
@cat $(TEMPLATES_DIR)/footer.html >> $@
For a simple listing this should be good enough.
The benefits of this setup?
- No dependencies - Just standard Unix tools
- Fast builds - Make only rebuilds what changed
- Easy to understand - No magic, just simple commands
- Version control friendly - Everything is plain text
Example Workflow
# Create a new post
vim src/content/my-new-post.md
# Build the site
make all
# Preview locally
make serve
# Clean and rebuild
make clean && make allThat’s it! Simple and effective.
What does it NOT have?
- Image optimization
- Minification
- RSS feed generation
- Sitemap creation
- Draft post support
These can be added by calling external tooling ofcourse from the Makefile, but that kind of defeats the purpose and makes a static website framework like Hugo or Jekyll a more likely candidate again. For now, my Makefile setup suits me and helps me focus on content. In a simple way.