/~ ZAP

zap is a static site generator


zap

A wesbat@tilde.town production Copyright (C) 2026

zap is a minimalist and opinionated static site generator written in Python. The code lives in a single file and is under 300 lines of code.

Out of the box zap supports page templates via the power of Jinja, you can use it to generate pages, journal entries or a blog.

You can use zap as-is, or if you like to get your hands dirty, extend zap's functionality by writing Python functions which can be called from within your HTML templates and markdown content.

Let's get crafty!

Get It

Current version is 1.2

To check your current version run zap --version. To update your version cd to your copy of zap and run git pull.

For new installations follow these steps:

# Create your own personal copy of zap
git clone ~wesbat/code/zap

# Install requirements
cd zap
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

# Create a symlink to the program
mkdir -p ~/bin
ln -s $(pwd)/zap.sh ~/bin/zap

The above command assumes that ~/bin is in your $PATH environment variable.

You now have the zap command.

Usage

usage: zap <content> <templates> <output>

zap takes three paths: where your markdown <content> lives, where your <templates> live, where to write the <output>.

zap scans recursively for .md files in the <content> path, renders their HTML and marries them with a template, and writes the result to the <output> path.

The directory hierarchy of your content is preserved, so you can create pages in whatever structure you like best.

Website From Scratch

This tour will walk you through creating a new site from the ground up. This will show how zap operates while giving you an rough idea how to get started.

To keep this tour simple we won't use any advanced Jinja features, and our site won't have any style sheet. Let's go!

Structure

Our website will have topical pages (about, hobbies) and blog posts. The main landing page will also list all other pages and posts on our site, so that new content appears in the index auto-magically.

To illustrate the structure of content vis-a-vis generated output:

├── content                    ->   output/
│   ├── homepage.gif           ->   ├── homepage.gif
│   ├── index.md               ->   ├── index.html
│   ├── pages                  ->   ├── pages
│   │   ├── about.md           ->   │   ├── about.html
│   │   └── hobbies.md         ->   │   └── hobbies.html
│   └── posts                  ->   └── posts
│       ├── 2000-01-01.md      ->       ├── 2000-01-01.html
│       ├── 2026-04-16.md      ->       ├── 2026-04-16.html
│       ├── 2026-05-10.md      ->       ├── 2026-05-10.html
│       └── electric-zap.mp3   ->       └── electric-zap.mp3
└── templates
    ├── index.html
    ├── page.html
    └── post.html

Commands to create this structure:

mkdir -p content/pages
mkdir -p content/posts
mkdir templates
mkdir output

HTML Templates

Each markdown file can be associated with a template. When zap runs it renders the markdown and marries it into the template. It's that simple.

Let's start easy and create the templates for templates/page.html and templates/post.html, for now both these templates have the exact same HTML:

<html>
    <body>
        <h1> {{ title }} </h1>
        <a href="/"> go Back </a>
        <p>
          <em> {{ date }} </em>
        </p>
        {{ rendered_markdown }}
    </body>
</html>

We put {{ mustache }} braces that will be replaced when zap runs. title and date are our own fields (they are not hard-coded into zap). We will define the actual values for title & date in our content, later. You can make up any fields you need for your own site.

{{ rendered_markdown }} is specific to zap, this is where your markdown content is inserted, after it has been converted into HTML.

Finally we create templates/index.html for our main landing page. Later we will add logic therein to list all our other pages, but for now we keep it simple to get our site up and running:

<html>
    <body>
        <h1> welcome to {{ title }}! </h1>
        <img src="homepage.gif" width="86" height="88">
        {{ rendered_markdown }}
    </body>
</html>

Content Overview

Content is written in Markdown syntax in files with the .md extension.

Content files contain front-matter: extra data for that page, like its title or date. You can put whatever values you want in the front-matter, it's there for you to use!

Front-matter is concluded with three dashes ---, after that is your content.

title: example content illustrates front-matter
template: page.html
---

This paragraph will render as **HTML** and get inserted into `page.html`.

There is no hard-coded expectation for what goes in your front-matter, however if you do provide a template value, your content will be rendered into that template.

Without a template, your content is still written to output as HTML, albeit it won't have any HTML/BODY tags.

Creating Content

We'll create our about & hobbies pages, and write a blog post.

our pages

Create content/pages/about.md:

title: about me
template: page.html
---

This page is all about me

Create content/pages/hobbies.md:

title: my hobby projects
template: page.html
---

Here are my hobby projects:

* Coding
* Music
* Reading
our blog entry

Create content/posts/2000-01-01.md:

title: Y2K
date: January 2000
template: post.html
---

Travelling back in time like it's Y2K

Note that our blog post has a date. This value will be inserted into our post.html template via the {{ date }} moustache.

our index

Lastly our main index, it will link to our other pages and blog entry. You will notice we use markdown-style links [title](url), and that they point to .html files.

Create content/index.md:

title: my homepage
template: index.html
---

Welcome to my site!

### Pages

* [about me](pages/about.html)
* [hobbies](pages/hobbies.html)

### Posts

* [Y2K](posts/2000-01-01.html)

Building Our Site

Finally we can tell zap to build our site. The verbose option prints out what is happening.

zap content/ templates/ output/ --verbose

templates found:
- page.html
- index.html
- post.html
content found:
- content/posts/2000-01-01.md -> output/posts/2000-01-01.html
- content/index.md -> output/index.html
- content/pages/about.md -> output/pages/about.html
- content/pages/hobbies.md -> output/pages/hobbies.html
processing content/posts/2000-01-01.md
header: {'title': 'Y2K', 'date': 'January 2000', 'template': 'post.html'}
processing content/index.md
header: {'title': 'my homepage', 'template': 'index.html'}
processing content/pages/about.md
header: {'title': 'about me', 'template': 'page.html'}
processing content/pages/hobbies.md
header: {'title': 'my hobby projects', 'template': 'page.html'}
Write output/posts/2000-01-01.html
Write output/index.html
Write output/pages/about.html
Write output/pages/hobbies.html

Automatic Index

Our site looks great, we can create new pages and posts on a whim, but we still need to manually include them on our index. Let's make our index generate a list of pages and posts automatically.

We accomplish this by using a for-loop. zap uses Jinja to render, so we have access to the full Jinja API in our templates :)

The Variables section of this document, below, lists all the special variables that your templates have access to. index is one such variable, it's a list of all the content on your entire site. We will loop over index to generate the index page.

First let's remove our hard-coded links from index.md ...

title: my homepage
template: index.html
---

Welcome to my site!

Then update templates/index.html to loop over each index entry, testing its template value for either "page.html" or "post.html", to separate the two categories of pages:

<html>
    <body>
        <h1> welcome to {{ title }}! </h1>
        <img src="homepage.gif" width="86" height="88">

        {{ rendered_markdown }}

        <h3> Pages </h3>
        <ul>
          {% for page in index if page.template == 'page.html' %}
          <li>
            <a href="{{ page.url }}"> {{ page.title }} </a>
          </li>
          {% endfor %}
        </ul>

        <h3> Posts </h3>
        <ul>
          {% for post in index if post.template == 'post.html' %}
          <li>
            <a href="{{ post.url }}"> {{ post.title }} </a>
          </li>
          {% endfor %}
        </ul>

    </body>
</html>

Any new pages or posts will appear on our index. Neat, right?

Now we run zap content/ templates/ output/ --verbose again and admire our site in our favourite web browser.

Static Files

zap does not process any assets (images, css, et cetera) in your <content> directory, this is a job well suited for rsync though. After your site is built you can rsync any assets to <output> like so:

rsync -r --exclude="*.md" content/* output/

I highly recommend using a command runner (make or just) to quickly run zap and rsync in one step.

Take a look at zap's own justfile for recipes to build, clean and serve your site!

Variables

Templates use the Jinja engine. {{ mustache }} braces are replaced with variables from your content's front-matter, and the following variables are available within your templates.

Variable Description
{{ rendered_markdown }} The rendered HTML of your markdown file.
{{ current }} The URL of the current page relative to <content>.
{{ index }} List of all pages in your site, each a dict of front-matter values, including a url value.
{{ prefix }} The value given to the --prefix command parameter.

Custom functions

For advanced users, you can write your own Python functions in zapfuncs.py, to make them available within your HTML templates and in markdown content.

Functions can be called via the moustache syntax, {{ timestamp() }}. To see examples of how to write and use this feature, look at the example/ site in the zap source code.

More examples

You can browse my tilde homepage source, to see how I use zap: ~wesbat/code/homepage.


That's all folks.