Jinja is the template language Flask uses to produce HTML on the
server. A template is HTML with holes; render_template()
fills the holes from the keyword arguments the route passes and returns
the finished string. By the time the browser sees anything, every trace
of Jinja is gone β which is why "view source" on this site shows plain
HTML.
{{ nutrient.label }} β expression: evaluate and insert
{% if ai_enabled %} β¦ {% endif %} β statement: control flow
{# never reaches the browser #} β comment (unlike <!-- HTML comments -->, which do)
The values come from the route. Here's the pair β server side and template side β for the log page:
app.py βreturn render_template('log.html', user=g.user,
nutrient=config.resolve_nutrient(g.user),
fiber_guide=config.FIBER_GUIDE,
ai_enabled=bool(config.OPENAI_API_KEY),
photos_enabled=bool(config.S3_BUCKET))
templates/log.html β
{% if ai_enabled %}
<div class="ai-row">
<button type="button" class="btn subtle" id="estimateBtn">β¦</button>
</div>
{% endif %}
Note what that conditional achieves: when the operator hasn't configured an OpenAI key, the AI buttons don't render at all. Not hidden with CSS β absent from the HTML. The server knows things at render time that the browser shouldn't have to discover (or shouldn't be told), and this is the mechanism.
Templates form a class-like hierarchy. base.html defines
the skeleton and marks the variation points as named blocks; each page
extends it and overrides only what it needs:
{% extends "base.html" %}
{% block title %}Ndiro β Log{% endblock %}
{% block styles %} β¦page-specific CSSβ¦ {% endblock %}
{% block content %} β¦the page bodyβ¦ {% endblock %}
{% block scripts %} β¦the page's JSβ¦ {% endblock %}
That's why every page shares the theme tokens, the corner menu, and
the brand pill without repeating a line of them. There's also
composition via include, and Ndiro uses it for something
subtle: the review page and the public share view render the
same chart-and-feed partial,
_review_core.html, with different data URLs. The share
view has no edit buttons and no AI affordances not because someone
remembered to strip them, but because the shared partial never had them
β reuse as a security guarantee.
One more supporting piece: a context processor in
app.py (inject_build) adds
build_commit_short to every template's context
automatically β that's how the running commit shows up in the menu on
all pages without every route passing it.
The most important Jinja fact for a reviewer. When
{{ meal.description }} inserts a value, Jinja
HTML-escapes it: < becomes <,
" becomes ". So a meal named
<script>alert(1)</script> renders as those
literal characters instead of executing β the cross-site-scripting
(XSS) attack dies by default. The repo's tests exercise exactly this:
tests/test_m9_status.py injects a script tag through the
commit title and asserts it comes out inert.
The escape hatch, a |safe filter, marks a value as
trusted markup and skips escaping. This codebase never uses it on user
data β treat any new |safe in a diff as a finding until
proven otherwise.
tojson: the one sanctioned bridge to JavaScriptSometimes the server needs to hand a data structure to the page's
JavaScript. Interpolating into a script with plain
{{ β¦ }} is wrong (HTML escaping isn't JS escaping);
building JSON by string concatenation is worse. The right tool:
<script>
const FIBER_GUIDE = {{ fiber_guide|tojson }};
const NUTRIENT = {{ nutrient|tojson }}; // {key, label, unit, goal, β¦}
</script>
|tojson serializes the Python value as JSON and
escapes it safely for a script context. The rendered page simply
contains const NUTRIENT = {"key": "fiber_g", β¦}; β server
state becomes a JS constant. This is the seam from the
anatomy chapter made concrete: config crosses the boundary once, at render time, through
this filter.
With two places to compute things, the split rule in this codebase is: identity, configuration, and anything security-relevant render server-side; anything derived from fetched data or local time renders client-side. The nutrient label is Jinja (the server resolved it); the "Today β Thu, Sep 3" heading is JavaScript (only the browser knows the user's timezone β the server clock is deliberately never trusted for user-local dates, a rule you'll find as invariant #10 in CLAUDE.md).
A template is a prepared statement for HTML. Autoescaping is
parameter binding: values can never be confused for syntax. String
concatenation into HTML is exactly SQL-injection-by-concatenation,
same failure mode, same fix. And |safe is the raw-query
escape hatch every ORM has β legitimate, rare, and where the
audits go.
Log a meal whose description is
<b>bold?</b>. It displays as literal text
with the angle brackets visible. Then find it in devtools β Elements
and note the text node β the markup arrived escaped. (This
particular render happens in JavaScript via
textContent, the DOM-side twin of autoescaping β
the JavaScript chapter picks up that
thread.)