Three Python static site generators. Three different design philosophies.
TL;DR
| Bengal | Pelican | MkDocs | |
|---|---|---|---|
| Best for | Docs, blogs, product sites | Blogs, content-heavy | Documentation |
| Object model | Generic (Page, Section, Asset) | Blog-centric (Article, Author, Tag) | Docs-centric (nav, sections) |
| Built-in | Sitemap, RSS, search, social cards | RSS; sitemap/search via plugins | Search, sitemap |
| Templating | Kida (Jinja2-like) | Jinja2 | Jinja2 (Material, etc.) |
| Markdown | Patitas (MyST) | Markdown, reST | Markdown |
| Parallel builds | Yes (3.14t) | No | No |
| Incremental | Yes (sub-second) | Limited | Limited |
| Python | 3.14+ | 3.8+ | 3.8+ |
Build speed and parallelism
| Bengal | Pelican / MkDocs | |
|---|---|---|
| Parallel rendering | Yes (3.14t, ThreadPoolExecutor) |
No |
| Large sites | Parallel, scales with workers | Linear with page count |
| Incremental | Yes — affected pages only | Limited / none |
| Single-page edit | Sub-second | Full rebuild |
Bengal is built for free-threaded Python. Pelican and MkDocs run on traditional Python; build times scale roughly linearly.
Content model
| Bengal | Pelican | MkDocs | |
|---|---|---|---|
| Markdown | Patitas (MyST) | Markdown, reST | Markdown + Material |
| Directives | Callouts, admonitions, figures | — | — |
| Structure | Sections + _index.md |
Similar + index.md |
mkdocs.yml defines nav |
Object models
Each SSG has its own object model — the types it uses to represent content and structure.
| Bengal | Pelican | MkDocs | |
|---|---|---|---|
| Core types | Site, Page, Section, Asset, Theme |
Article, Page, Category, Tag, Author |
Docs, nav, sections |
| Content types | Generic pages (blog, docs, product) | Articles vs pages; blog-centric | Documentation |
| Taxonomy | Optional (via frontmatter) | Built-in categories, tags, authors | Nav hierarchy |
| Hierarchy | Sections (directories) | Categories, tags | Nav tree |
| Bengal | Pelican | MkDocs | |
|---|---|---|---|
| Philosophy | Content-agnostic | Blog-centric | Docs-first |
| Template vars | page (all content) |
article (posts), page (static) |
docs, nav |
| Content type | One Page type |
Article vs Page |
Nav hierarchy |
Batteries included
All three ship with useful defaults, but the kind of batteries differs.
| Bengal | Pelican | MkDocs | |
|---|---|---|---|
| Sitemap | Built-in (XML) | Plugin | Built-in (XML) |
| RSS/Atom | Built-in | Built-in | No (docs-only) |
| Search | Built-in (Lunr, index.json) | Plugin | Built-in (Lunr) |
| 404, robots.txt | Built-in | Theme-dependent | Built-in |
| Social cards, Open Graph | Built-in | Theme-dependent | Theme-dependent |
| Output formats | JSON, LLM txt, index.json | — | — |
Bengal — built-in (no plugins):
- Sitemap, RSS, search indexes (Lunr, index.json)
- Canonical URLs, Open Graph, Twitter cards
- Machine outputs:
index.json,llm-full.txt
MkDocs — built-in for docs:
- Search (Lunr), sitemap
- No RSS (docs-only)
- Social metadata: theme-dependent (e.g. Material)
Pelican — built-in vs plugins:
- Built-in: RSS/Atom feeds
- Plugins: sitemap, search
- Strength: plugin ecosystem; you add what you need
Templating
| Bengal | Pelican | MkDocs | |
|---|---|---|---|
| Engine | Kida | Jinja2 | Jinja2 (Material, etc.) |
| Syntax | {{ variable }}, {% for %}, {% block %} |
Same | Same |
| Extras | Static analysis (required_context) |
— | — |
| Free-threading | Yes | No | No |
Incremental builds
| Bengal | Pelican | MkDocs | |
|---|---|---|---|
| On file change | Rebuild affected only | Full rebuild | Full rebuild |
| Cache | Zstandard, provenance-based | --ignore-cache only |
None |
| Typical time | Sub-second | Full rebuild | Full rebuild |
Same page, three templates
A production-style layout with null coalescing and optional metadata. Kida supports ?. (optional chaining) and ?? (null coalescing); Jinja2 uses | default() and defensive conditionals.
{% extends "base.html" %}
{% block title %}{{ page.title | default(site.title) }}{% end %}
{% block content %}
<article>
<h1>{{ page.title ?? 'Untitled' }}</h1>
{% if page.date %}
<time>{{ page.date | dateformat("%Y-%m-%d") }}</time>
{% end %}
{{ page.content | safe }}
{% set links = params?.quick_links ?? [] %}
{% for link in links %}
<a href="{{ link?.href ?? link?.url ?? '#' }}">{{ link?.title ?? 'Link' }}</a>
{% end %}
</article>
{% end %}
{% extends "base.html" %}
{% block title %}{{ article.title | default(site.title) }}{% endblock %}
{% block content %}
<article>
<h1>{{ article.title | default('Untitled') }}</h1>
{% if article.date %}
<time>{{ article.date.strftime('%Y-%m-%d') }}</time>
{% endif %}
{{ article.content }}
{% set links = (article.quick_links if article else []) or [] %}
{% for link in links %}
<a href="{{ (link.href or link.url) or '#' }}">{{ link.title | default('Link') }}</a>
{% endfor %}
</article>
{% endblock %}
# Page Title
Page content in Markdown. Material theme handles layout.
Nav comes from mkdocs.yml — no template inheritance.
No optional chaining or null coalescing in templates.
| Kida | Jinja2 | |
|---|---|---|
| Null coalescing | ?? |
| default() |
| Optional chaining | ?. |
if x else y |
| Block end | {% end %} (all blocks) |
{% endblock %}, {% endfor %}, {% endif %} |
| Variable names | page |
page or article |
Where each shines (and stumbles)
Shines: Parallel builds on 3.14t. Sub-second incremental rebuilds. Provenance-based cache. MyST directives (callouts, tabs, figures). Static analysis for block caching.
Stumbles: Newer ecosystem. Fewer plugins than Pelican. Python 3.14+ only. Kida syntax uses {% end %} instead of {% endblock %} — small migration cost from Jinja2.
Shines: Mature (since 2010). Huge plugin ecosystem. reStructuredText support. Themes and community. Works on Python 3.8+.
Stumbles: No parallel rendering. No true incremental builds. Full rebuild every time. GIL-bound for CPU work.
Shines: Simple config. Material theme is polished. Great for docs. Fast to get started. Nav in YAML, not templates.
Stumbles: Docs-only focus. No blog primitives (tags, dates, feeds) without plugins. Full rebuild. Not designed for product sites or complex layouts.
| Shines | Stumbles | |
|---|---|---|
| Bengal | Parallel builds, sub-second incremental, MyST directives, static analysis | Newer, fewer plugins, 3.14+ only |
| Pelican | Mature (2010), huge plugins, reST | No parallel, full rebuild, GIL-bound |
| MkDocs | Simple config, Material theme, fast start | Docs-only, no blog primitives, full rebuild |
Config comparison
# bengal.yaml
site:
title: My Site
base_url: https://example.com
# pelicanconf.py
AUTHOR = "You"
SITENAME = "My Site"
SITEURL = "https://example.com"
# mkdocs.yml
site_name: My Site
site_url: https://example.com
| Bengal | Pelican | MkDocs | |
|---|---|---|---|
| Config format | YAML | Python | YAML |
| Tradeoff | Declarative | Flexible, in code | Declarative |
When to choose Bengal
- You have hundreds of pages and care about build speed
- You want incremental rebuilds for a fast editing loop
- You're on Python 3.14 and want to use free-threading
- You need docs, blog, or product site with sitemaps, feeds, and search indexes
See Best Python Static Site Generators for 2026 for the full roundup.
When to choose Pelican or MkDocs instead
| Use case | Choice | Why |
|---|---|---|
| Mature ecosystem, lots of plugins | Pelican | Since 2010, strong community |
| Docs only, simple setup | MkDocs | Opinionated, excels at docs |
| Blogs or product sites | Pelican or Bengal | MkDocs not designed for complex layouts |