Stapler
A simple static site generator built with Jinja and Markdown.
Installation
Clone the repo:
git clone https://github.com/gijs6/stapler.git
cd stapler
Create a virtual environment (recommended):
python -m venv .venv
source .venv/bin/activate
Install:
pip install -e .
Or with dev dependencies:
pip install -e ".[dev]"
Quick start
- Create a
stapler.tomlin your project root:
[site]
url = "https://yoursite.com"
title = "Your site"
-
Put your content in a
site/directory (the default). Templates go insite/templates/. -
Run:
stapler serve # local dev server on port 8000
stapler build # production build
Configuration
Required
[site]
url = "https://yoursite.com"
title = "Your site"
Optional
Site metadata
[site]
description = "About your site"
base_path = "/blog" # Deploy to example.com/blog instead of the root
Author info
Used in RSS/Atom feeds.
[site.author]
name = "Your name"
email = "you@example.com"
Directories
All paths are relative to where you run stapler.
[directories]
site = "site" # Content directory (default: "site")
build = "build" # Production output (default: "build")
build_dev = "build-dev" # Dev server output (default: "build-dev")
templates = "templates" # Templates folder inside the site directory (default: "templates")
blog = "blog" # Blog posts folder inside the site directory (default: "blog")
Default template
The template used for HTML pages that have front matter but no template field.
[templates]
default = "base.html"
Blog
[features.blog]
enabled = true # Enable blog functionality (default: false)
template = "blog_post.html" # Template for individual posts
index_template = "blog_index.html" # Template for the blog index page
Sitemap and feeds
[features]
sitemap = true # Generate sitemap.xml (default: true)
[features.feeds]
rss = true # Generate rss.xml (default: true)
atom = true # Generate atom.xml (default: true)
Feeds are only generated when the blog feature is enabled.
Markdown extensions
[markdown]
extensions = ["meta", "tables", "fenced_code"] # Python-Markdown extensions
How it works
Pages
Any .html or .md file in your site directory (excluding the templates and blog folders) becomes a page.
Markdown files
Markdown files are always rendered to HTML. If the front matter includes a template field, the result is passed to that template as page.content. Without a template field (or without front matter entirely), the raw HTML is written directly.
---
template: base.html
title: My page
---
# Content
Regular markdown here
HTML files
HTML files with front matter are rendered through a template. The template field in front matter takes precedence; if omitted, the default template from [templates].default is used.
---
title: My page
---
<h1>Content here</h1>
HTML files without front matter are treated as Jinja templates directly:
{% extends "base.html" %}
{% block content %}
<h1>Hello</h1>
{% endblock %}
Front matter is YAML. All fields are available as page.metadata.<field> in your templates.
Static files
Anything that's not a .html or .md file (and not in your templates or blog folder) is copied as-is to the output directory.
Blog
Enable the blog feature in your config, then put .md files in your blog directory.
---
title: My post
date: 2025-01-15
---
Post content here
The date field is optional. If omitted, stapler tries to infer it from the file's git history.
Templates
Templates live in the directory you configured (default: site/templates/).
Available in all templates
data: build infodata.now: current build timedata.now.date.long: date as%B %d, %Y(e.g.April 12, 2026)data.now.date.short: date as%Y-%m-%d(e.g.2026-04-12)data.now.time: time as%H:%M:%Sdata.now.iso: datetime as ISO 8601
data.last_commit: last git commit info (Noneif not in a git repo)data.last_commit.hash.short: short 7-character commit hashdata.last_commit.hash.long: full commit hashdata.last_commit.dt.date.long: date as%B %d, %Y(e.g.April 12, 2026)data.last_commit.dt.date.short: date as%Y-%m-%d(e.g.2026-04-12)data.last_commit.dt.time: time as%H:%M:%Sdata.last_commit.dt.iso: datetime as ISO 8601
Regular page templates
page: the current pagepage.active_page: identifier derived from the filename (e.g.aboutforabout.html,homeforindex.html), useful for highlighting the active nav itempage.canonical_path: URL path of the page (e.g./about)page.content: page content as HTML (only present if the page has front matter)page.metadata.<field>: any front matter field (e.g.page.metadata.title)
Blog post template
page: navigation infopage.active_page: name of the blog directory (e.g.blog)page.canonical_path: URL path of the post (e.g./blog/my-post)
post: the current blog postpost.title: post title (from front matter, or derived from the filename)post.slug: URL slug (filename without.md)post.content: post content as HTMLpost.date: date as%Y-%m-%d(e.g.2026-04-12), only set if a date is availablepost.date_iso: date as ISO 8601, only set if a date is available
data: same as above
Blog index template
page: navigation infopage.active_page: name of the blog directory (e.g.blog)page.canonical_path: URL path of the blog index (e.g./blog)
posts: list of all blog posts sorted newest first; each item has the same fields aspostabovedata: same as above
CLI
# Build with default config (stapler.toml)
stapler build
# Build with custom config
stapler build -c myconfig.toml
# Serve on default port (8000)
stapler serve
# Serve on custom port
stapler serve -p 3000
# Serve with custom config and port
stapler serve -c myconfig.toml -p 3000
# Show version
stapler --version
Metadata
Release files for stapler-ssg 0.1.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| stapler_ssg-0.1.3.tar.gz | 12.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| stapler_ssg-0.1.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 26.2 kB
Release files / stapler_ssg-0.1.3.tar.gz
| Download URL | stapler_ssg-0.1.3.tar.gz |
|---|---|
| Size | 12.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4e4a61b1d9337c325b3619fdc385c174f3d8293c6e0436495dfaadb843e0d5d1
|
|
BLAKE2b-256 checksum How to use checksums |
5c4b64422f529f991cb913875a672a8e4f48a33692625e2c2241238afd566898
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logRelease files / stapler_ssg-0.1.3-py3-none-any.whl
| Download URL | stapler_ssg-0.1.3-py3-none-any.whl |
|---|---|
| Size | 14.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
214b9b7d826c0ceef05bd31c5690c011aaf4c63919df74e43e64d7c058b13c44
|
|
BLAKE2b-256 checksum How to use checksums |
23e62337c80451703fc18aa6429b0f6afccf323c236762aac13f4e93e814d8ca
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency log