Jinjabread
A Python-based static site generator using Jinja templates.
Inspired by staticjinja, jekyll, and hugo.
Install
Requires Python 3.11 or newer.
pip install jinjabread
Usage
Create new site project
python -m jinjabread new mysite
Build site
python -m jinjabread build mysite
Preview site locally
python -m jinjabread serve mysite
# Visit http://127.0.0.1:8000 in your browser.
Features
- Write pages in Markdown, HTML, or text.
- Use Jinja2 templating language in Markdown, HTML, or text.
- Supports YAML metadata in Markdown pages.
- Keep static media alongside static pages.
- Index pages (i.e.,
index.html) can list all other pages in the same directory. - Preview your static site locally with a built-in web server.
- Prettify all generated HTML (because why not?)
HTML pretty-printing
With prettify_html enabled (the default), jinjabread re-indents every generated page for readability. It is a rendering-preserving reformatter rather than a byte-preserving one, and it makes a specific contract:
- It preserves how the page renders. The output displays identically to the input: inline elements are never broken across lines, significant whitespace is kept, and
pre,textarea,script, andstylecontent is emitted verbatim. Only insignificant whitespace between block-level elements changes. - It is idempotent. Prettifying already-pretty output changes nothing.
- It ends every file with a single newline. Non-empty output is terminated with exactly one
\n, following the POSIX convention that a text file's last line ends in a newline. Empty input stays empty. - It adds no wrapper of its own. A body-level fragment, such as several top-level
<p>elements or plain text, is emitted as-is and is never wrapped in a<div>or<span>. - It parses and repairs markup. Every page round-trips through lxml's HTML parser, so unclosed tags are closed and misnested tags are corrected, matching how a browser reads the input. The output is always well-formed HTML; invalid or non-standard structure is not preserved verbatim.
- It distinguishes documents from fragments. A whole document, or a document-level element such as a lone
<script>or<head>, is emitted as a complete document with an HTML5 doctype; a body-level fragment is emitted without a doctype or wrapper.
File structure
Important files and directories
| Name | Description | Example path |
|---|---|---|
| Project directory | The project root | mysite |
| Content directory | Contains site content where each file becomes a site page | mysite/content |
| Layouts directory | Contains page layouts that gets used by the site content | mysite/layouts |
| Static directory | Contains static media that gets copied to the output directory | mysite/static |
| Output directory | The complete generated site, ready to be hosted | mysite/public |
| Config file | Custom site configurations in TOML format | mysite/jinjabread.toml |
Example: Site project structure
mysite/
├── content
│ ├── about.html
│ ├── index.html
│ └── posts
│ ├── index.html
│ ├── my-journey
│ │ ├── index.html
│ │ └── profile-photo.jpg
│ └── my-story.html
├── jinjabread.toml
├── layouts
│ └── markdown.html
├── public
│ ├── about.html
│ ├── index.html
│ ├── posts
│ │ ├── index.html
│ │ ├── my-journey
│ │ │ ├── index.html
│ │ │ └── profile-photo.jpg
│ │ └── my-story.html
│ └── static
│ └── style.css
└── static
└── style.css
Example: File to URL translation
| File path | URL path |
|---|---|
public/index.html |
/ |
public/about.html |
/about |
public/posts/index.html |
/posts/ |
public/posts/my-story.html |
/posts/my-story |
public/posts/my-journey/index.html |
/posts/my-journey/ |
public/posts/my-journey/profile-photo.jpg |
/posts/my-journey/profile-photo.jpg |
public/static/style.css |
/static/style.css |
Site config
Default config
content_dir = "content"
layouts_dir = "layouts"
static_dir = "static"
output_dir = "public"
prettify_html = true
[context]
[[pages]]
type = "jinjabread.MarkdownPage"
glob_pattern = "**/*.md"
layout_name = "markdown.html"
[[pages]]
type = "jinjabread.Page"
glob_pattern = "**/*"
Custom config
Change output directory
# jinjabread.toml
output_dir = "dist"
Add global Jinja context variables
# jinjabread.toml
[context]
site_name = "My site"
url_origin = "https://mysite.com"
Change Markdown layout file
# jinjabread.toml
[[pages]]
type = "jinjabread.MarkdownPage"
glob_pattern = "**/*.md"
layout_name = "post.html"
[[pages]]
type = "jinjabread.Page"
glob_pattern = "**/*"
Add page-specific Jinja context variables
# jinjabread.toml
[[pages]]
type = "jinjabread.MarkdownPage"
glob_pattern = "**/*.md"
layout_name = "markdown.html"
[[pages]]
type = "jinjabread.Page"
glob_pattern = "**/*.txt"
[pages.context]
foo = "bar"
[[pages]]
type = "jinjabread.Page"
glob_pattern = "**/*"
Pages types
| Page type | Keyword arguments |
|---|---|
jinjabread.Page |
- glob_pattern |
jinjabread.MarkdownPage |
- glob_pattern - layout_name |
Markdown pages
Markdown content supports full YAML metadata.
For example, given the following content and layout:
---
# content/my-blog-post.md
title: My blog post
author: John Smith
description: A very nice story.
keywords:
- thrilling
- must-read
---
It was a cold stormy night...
<!-- layouts/markdown.html -->
<h1>{{ title }}</h1>
<h2>{{ description }}</h2>
<h3>Written by {{ author }}</h3>
{{ content }}
Results in the following output:
<!-- public/my-blog-post.html -->
<h1>
My blog post
</h1>
<h2>
A very nice story.
</h2>
<h3>
Written by John Smith
</h3>
<p>
It was a cold stormy night...
</p>
Context variables
Default variables
All pages have these context variables:
| Name | Description | Example value |
|---|---|---|
url_path |
The URL path of the current page | / |
file_path |
The file path of the current page, relative to the output directory | index.html |
Variable precedence
Page-specific context variables override global site context variables.
For example:
# jinjabread.toml
[context]
fee = "fie"
foe = "fum"
[[pages]]
type = "jinjabread.Page"
glob_pattern = "**/*.txt"
[pages.context]
foe = "foo"
[[pages]]
type = "jinjabread.Page"
glob_pattern = "**/*.html"
[pages.context]
foe = "bar"
.txtpages will have the following extra context variables:fee = "fie" foe = "foo"
.htmlpages have the following extra context variables:fee = "fie" foe = "bar"
Index pages
All index pages (i.e., index.*) has an extra context variable named pages which is list of dictionaries of context variables from its sibling files.
For example, given the following file structure:
mysite/content/posts/
├── index.html
├── post1.md
└── post2.md
You can list all the pages in the posts directory using:
<!-- mysite/content/posts/index.html -->
{% for page in pages %}
<a href="{{ page.url_path }}">
{{ page.url_path.split('/') | last | title }}
</a>
{% endfor %}
Resulting in:
<!-- mysite/public/posts/index.html -->
<a href="/posts/post1">Post1</a> <a href="/posts/post2">Post2</a>
Contributing
Setup
Install uv (https://docs.astral.sh/uv/getting-started/installation/), then:
uv sync
Test
uv run python -m unittest
Build
uv build
Release
export TWINE_USERNAME='__token__'
export TWINE_PASSWORD='secret-token'
uv run twine upload dist/*
Metadata
Release files for jinjabread 0.7.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| jinjabread-0.7.0.tar.gz | 106.5 kB | Without provenance | Details for jinjabread-0.7.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| jinjabread-0.7.0-py3-none-any.whl | Python 3 | none | any | Details for jinjabread-0.7.0-py3-none-any.whl |
Total release size: 122.3 kB
Release files / jinjabread-0.7.0.tar.gz
| Download URL | jinjabread-0.7.0.tar.gz |
|---|---|
| Size | 106.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0185ce57b6b9d7c53eb3aba5bd21e3d15afbd0ac117fd5358a9f5b654de7e953
|
|
BLAKE2b-256 checksum How to use checksums |
a2d64831fb5f78fd819163150bc8a439055d6ace1f2335eb5708bd6fe508ccaa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|
Release files / jinjabread-0.7.0-py3-none-any.whl
| Download URL | jinjabread-0.7.0-py3-none-any.whl |
|---|---|
| Size | 15.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ccf9e139ab325c7439ab859f62932a6f4e409ca4097b4692387636a3907440e5
|
|
BLAKE2b-256 checksum How to use checksums |
21ddd9c25aa3f9add9ca8f46009fddfd4ce50bda962d6af1aebd50980191e3e8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|
Security for jinjabread 0.7.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.