MiniJinja Multi-page Blog¶
This example showcases a realistic, full-featured multi-page blog application using Cello and MiniJinja. It implements template layouts, pagination, filtering by tags, custom global template contexts (e.g. for sidebar widgets), and template-driven 404 page error handling.
Features Demonstrated¶
- Layout Structure: Hierarchical templates with
base.htmlproviding layout frameworks. - Pagination Logic: Pagination helpers using query parameters to slice in-memory mock datasets.
- Sidebar Integration: Computing dynamic values once (e.g. tag count and recent posts) and adding them to the global templating context.
- Error Page Templates: Rendering user-friendly error views using custom functions and setting a
404status code.
Complete Source Code¶
"""
MiniJinja Blog Example — Cello v1.1.0
A realistic multi-page blog with:
- Template inheritance (base → layout → page)
- Listing page with pagination
- Post detail page
- 404 error page via error handler
- Sidebar with recent posts (from a shared global helper)
Run:
python examples/minijinja_blog.py
Then visit:
http://localhost:8082/
http://localhost:8082/posts
http://localhost:8082/posts/1
http://localhost:8082/posts/99 ← triggers 404 template
http://localhost:8082/tag/python
"""
import os
import tempfile
from datetime import date
from cello import App, Response
# ---------------------------------------------------------------------------
# In-memory "database"
# ---------------------------------------------------------------------------
POSTS = [
{
"id": 1,
"slug": "hello-cello",
"title": "Hello, Cello!",
"summary": "Introducing the ultra-fast Rust-powered Python web framework.",
"body": "Cello is built on a Rust core with PyO3 bindings...",
"author": "Jagadeesh",
"date": "2026-01-10",
"tags": ["cello", "rust", "python"],
"reading_time": 3,
},
{
"id": 2,
"slug": "minijinja-templates",
"title": "Jinja2 Templates in Rust with MiniJinja",
"summary": "Full Jinja2 syntax at Rust speed — now built into Cello v1.1.0.",
"body": "MiniJinja is written by Armin Ronacher, the author of Jinja2...",
"author": "Jagadeesh",
"date": "2026-03-15",
"tags": ["cello", "templates", "minijinja"],
"reading_time": 5,
},
{
"id": 3,
"slug": "async-handlers",
"title": "Writing Async Handlers",
"summary": "How to use async/await in Cello route handlers for I/O-bound work.",
"body": "Cello supports both sync and async handlers transparently...",
"author": "Jagadeesh",
"date": "2026-04-01",
"tags": ["cello", "python", "async"],
"reading_time": 4,
},
{
"id": 4,
"slug": "rbac-guards",
"title": "Role-Based Access Control with Guards",
"summary": "Protect routes with composable RBAC guards in just one line.",
"body": "Cello's guard system lets you attach role and permission checks...",
"author": "Jagadeesh",
"date": "2026-04-08",
"tags": ["cello", "security", "rbac"],
"reading_time": 6,
},
]
# ---------------------------------------------------------------------------
# Templates
# ---------------------------------------------------------------------------
TEMPLATE_DIR = tempfile.mkdtemp(prefix="cello_blog_")
def tpl(name, content):
path = os.path.join(TEMPLATE_DIR, name)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w") as f:
f.write(content)
# base.html — outermost HTML skeleton
tpl("base.html", """\
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ site_name }}{% endblock %}</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body { font-family: Georgia, serif; margin: 0; color: #222; }
a { color: #1a73e8; text-decoration: none; }
a:hover { text-decoration: underline; }
.wrap { max-width: 900px; margin: 0 auto; padding: 0 1.5rem; }
header { background: #1a1a2e; color: #fff; padding: 1rem 0; }
header a { color: #a8d8ea; }
header h1 { margin: 0; font-size: 1.6rem; }
header p { margin: 0; font-size: 0.85rem; opacity: 0.75; }
.layout { display: grid; grid-template-columns: 1fr 280px; gap: 2rem;
padding: 2rem 0; }
main { min-width: 0; }
aside { border-left: 1px solid #eee; padding-left: 1.5rem; }
footer { border-top: 1px solid #eee; text-align: center;
padding: 1.5rem 0; font-size: 0.8rem; color: #888; }
.tag { display: inline-block; background: #e8f0fe; color: #1a73e8;
border-radius: 3px; padding: 1px 7px; font-size: 0.78rem;
margin: 2px; font-family: monospace; }
</style>
</head>
<body>
<header>
<div class="wrap">
<h1><a href="/">{{ site_name }}</a></h1>
<p>{{ tagline }}</p>
</div>
</header>
<div class="wrap layout">
<main>{% block content %}{% endblock %}</main>
<aside>{% block sidebar %}
<h3>Recent Posts</h3>
<ul>
{% for p in recent_posts %}
<li><a href="/posts/{{ p.id }}">{{ p.title }}</a></li>
{% endfor %}
</ul>
<h3>Tags</h3>
{% for tag in all_tags %}
<a class="tag" href="/tag/{{ tag }}">#{{ tag }}</a>
{% endfor %}
{% endblock %}</aside>
</div>
<footer>
<div class="wrap">© {{ year }} {{ site_name }} · Built with Cello {{ cello_version }}</div>
</footer>
</body>
</html>
""")
# index.html — home / post listing
tpl("index.html", """\
{% extends "base.html" %}
{% block title %}{{ site_name }} — Blog{% endblock %}
{% block content %}
<h2>Latest Posts</h2>
{% for post in posts %}
<article style="margin-bottom:2rem; padding-bottom:1.5rem; border-bottom:1px solid #eee;">
<h3 style="margin-bottom:0.25rem;">
<a href="/posts/{{ post.id }}">{{ post.title }}</a>
</h3>
<small style="color:#888;">
By {{ post.author }} · {{ post.date }} · {{ post.reading_time }} min read
</small>
<p>{{ post.summary }}</p>
{% for tag in post.tags %}
<a class="tag" href="/tag/{{ tag }}">#{{ tag }}</a>
{% endfor %}
</article>
{% else %}
<p>No posts yet.</p>
{% endfor %}
{# Pagination #}
{% if total_pages > 1 %}
<nav style="margin-top:1rem;">
{% if page > 1 %}
<a href="/posts?page={{ page - 1 }}">« Prev</a>
{% endif %}
Page {{ page }} of {{ total_pages }}
{% if page < total_pages %}
<a href="/posts?page={{ page + 1 }}">Next »</a>
{% endif %}
</nav>
{% endif %}
{% endblock %}
""")
# post.html — single post detail
tpl("post.html", """\
{% extends "base.html" %}
{% block title %}{{ post.title }} — {{ site_name }}{% endblock %}
{% block content %}
<article>
<h2>{{ post.title }}</h2>
<p style="color:#888; font-size:0.9rem;">
By {{ post.author }} · {{ post.date }} · {{ post.reading_time }} min read
</p>
{% for tag in post.tags %}
<a class="tag" href="/tag/{{ tag }}">#{{ tag }}</a>
{% endfor %}
<hr>
<p style="line-height:1.8;">{{ post.body }}</p>
</article>
<p style="margin-top:2rem;"><a href="/posts">← All Posts</a></p>
{% endblock %}
""")
# tag.html — posts filtered by tag
tpl("tag.html", """\
{% extends "base.html" %}
{% block title %}#{{ tag }} — {{ site_name }}{% endblock %}
{% block content %}
<h2>Posts tagged <span class="tag">#{{ tag }}</span></h2>
{% if posts %}
{% for post in posts %}
<article style="margin-bottom:1.5rem;">
<h3><a href="/posts/{{ post.id }}">{{ post.title }}</a></h3>
<small style="color:#888;">{{ post.date }}</small>
<p>{{ post.summary }}</p>
</article>
{% endfor %}
{% else %}
<p>No posts with this tag.</p>
{% endif %}
<p><a href="/posts">← All Posts</a></p>
{% endblock %}
""")
# 404.html
tpl("404.html", """\
{% extends "base.html" %}
{% block title %}404 Not Found — {{ site_name }}{% endblock %}
{% block content %}
<div style="text-align:center; padding:3rem 0;">
<h1 style="font-size:5rem; margin:0; color:#ccc;">404</h1>
<h2>Page Not Found</h2>
<p>{{ message }}</p>
<a href="/posts">← Back to all posts</a>
</div>
{% endblock %}
""")
# ---------------------------------------------------------------------------
# App
# ---------------------------------------------------------------------------
app = App()
# Collect sidebar data once
ALL_TAGS = sorted({tag for post in POSTS for tag in post["tags"]})
RECENT = POSTS[-3:][::-1] # latest 3
app.enable_templates(
template_dir=TEMPLATE_DIR,
auto_escape=True,
globals={
"site_name": "The Cello Blog",
"tagline": "Ultra-fast web development with Rust + Python",
"cello_version": "1.1.0",
"year": 2026,
"recent_posts": RECENT,
"all_tags": ALL_TAGS,
},
)
PAGE_SIZE = 2
@app.get("/")
def home(request):
html = app.render("index.html", {
"posts": POSTS[:PAGE_SIZE],
"page": 1,
"total_pages": -(-len(POSTS) // PAGE_SIZE), # ceiling division
})
return Response.html(html)
@app.get("/posts")
def post_list(request):
try:
page = int(request.params.get("page", "1"))
except ValueError:
page = 1
page = max(1, page)
total_pages = -(-len(POSTS) // PAGE_SIZE)
page = min(page, total_pages)
start = (page - 1) * PAGE_SIZE
html = app.render("index.html", {
"posts": POSTS[start: start + PAGE_SIZE],
"page": page,
"total_pages": total_pages,
})
return Response.html(html)
@app.get("/posts/{id}")
def post_detail(request):
try:
post_id = int(request.params["id"])
except ValueError:
return _not_found("Invalid post ID.")
post = next((p for p in POSTS if p["id"] == post_id), None)
if not post:
return _not_found(f"Post #{post_id} does not exist.")
html = app.render("post.html", {"post": post})
return Response.html(html)
@app.get("/tag/{tag}")
def tag_filter(request):
tag = request.params["tag"]
matched = [p for p in POSTS if tag in p["tags"]]
html = app.render("tag.html", {"tag": tag, "posts": matched})
return Response.html(html)
def _not_found(message: str):
html = app.render("404.html", {"message": message})
return Response.html(html, status=404)
if __name__ == "__main__":
print(f"Templates: {TEMPLATE_DIR}")
print("Listening on http://localhost:8082")
app.run(port=8082)
Running This Example¶
python examples/minijinja_blog.py
# Test endpoints:
curl http://127.0.0.1:8082/
curl http://127.0.0.1:8082/posts?page=2
curl http://127.0.0.1:8082/posts/1
curl http://127.0.0.1:8082/posts/99
curl http://127.0.0.1:8082/tag/rust
Key Concepts¶
- Pagination Arithmetic: Implementing clean index slices by combining integer ceiling division and maximum/minimum clamps on parsed parameters.
- Dynamic 404 Handler: Defining localized fallback rendering functions that present template error UI pages while returning the appropriate HTTP status codes.
- Nested Layout Inheritance: Using intermediate layout templates extending base frameworks to construct standard formats for site sidebars and primary columns.