Building a Static Site with Make

2025-12-10

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:

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?

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 all

That’s it! Simple and effective.

What does it NOT have?

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.