Skip to content

Templates#

Server-rendered HTML via a real tokenizer/parser/renderer — not a single regex, and never arbitrary code execution.

Rendering#

from hayavega.conf import load_settings
from hayavega.templates import render_template

_settings = load_settings()


def posts_page(request):
    posts = Post.collection(_settings.database).browse().values("title", "views")
    return Response(render_template("posts.html", {"posts": posts}, directories=(_settings.base_dir / "templates",)))

Syntax#

templates/posts.html
{% extends "base.html" %}

{% block content %}
  {% if posts %}
    <ul>
      {% for post in posts %}
        <li>{{ post.title }} ({{ post.views }} views){% if loop.first %} &mdash; newest{% endif %}</li>
      {% endfor %}
    </ul>
  {% else %}
    <p>No posts yet.</p>
  {% endif %}
{% endblock %}

Supported: {{ variable.path }} interpolation (auto-escaped), {% if/elif/else/endif %} with ==, !=, in, and, or, not, and string/number/true/false/none literals, {% for x in expr %} loops with loop.index/loop.index0/loop.first/loop.last, and {% extends %}/{% block %} multi-level inheritance.

Deliberately not supported: filters, macros, or fragments — and no arbitrary Python eval anywhere in the expression evaluator, by design. render_template(..., debug=True) raises a descriptive TemplateSyntaxError/TemplateRenderError (template name and line number) for an undefined variable, a non-iterable loop target, or an undeclared block; the default (debug=False) renders these as empty text instead, so a template typo degrades gracefully in production rather than 500ing.

Rendering flash messages#

consume_messages needs a Response to attach the cookie-clearing header to, but you need the message list before rendering the page that will display it. Get both in one call by consuming against an empty placeholder, then filling in the real body:

from dataclasses import replace

from hayavega.messages import consume_messages


def posts_page(request):
    messages, draft = consume_messages(request, Response(""))
    body = render_template("posts.html", {"posts": posts, "messages": messages}, directories=(_settings.base_dir / "templates",))
    return replace(draft, body=body)
templates/posts.html
{% if messages %}
  <ul class="messages">
    {% for message in messages %}
      <li class="{{ message.level }}">{{ message.text }}</li>
    {% endfor %}
  </ul>
{% endif %}

See Messages for the full API.

Translations#

The expression grammar above supports only dotted-path lookups -- no function calls, no filters -- so a translated string renders the same way any other context value does, via plain attribute access:

return Response(render_template("home.html", {"t": translator_for(request, catalog)}))
templates/home.html
<h1>{{ t.welcome }}</h1>

See i18n for catalogs, language negotiation, and pluralization.

Caching#

Templates are parsed once and cached per file, invalidated by mtime — an edited template is picked up on the next render with no server restart, while an unchanged one skips re-parsing entirely. A compile-to-real-Python-functions path (structure only — if/for control flow — not expression evaluation, which stays interpreted for its exact undefined-vs-None semantics) makes plain templates 1.27–2.19× faster; {% extends %} templates fall back to the interpreter automatically. See Performance for the full measurement, including why this specific win didn't move the published HTTP-level benchmark (the per-request SQLite connection cost dominates total request time far more than template rendering does).