/~ 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.