Frontend

Template resolution

We've been calling fluid.render("index.html") and extending fluid_base.html without ever explaining where those names actually point. This chapter pulls back the curtain — which matters, because the name you pass decides which file wins.

Rendering, the short version

fluid.render(template, **ctx) does two things: it runs every registered context processor (that's where the surface variables from the previous chapter come from) and then renders the template through the async Jinja environment. The template argument is a name, and that name is looked up against a stack of loaders.

Its sibling fluid.render_string(source, **ctx) does the same for a template you hold as a string instead of a file — same context processors, same environment, no lookup. Reach for it when the template comes from your database or from a user, and for nothing else: a string has no name, so it can neither be cached nor extended.

 

render() no longer takes an is_string flag. If you carry one over from an alpha it is not an error — it quietly becomes an ordinary template variable while your source string is looked up as a file name, and you get a TemplateNotFound naming your entire template. Use render_string.

Everything is escaped until you say otherwise

Before we get to where names point, one thing about what comes out. Every {{ value }} in an .html, .htm, .xml, .xhtml or .svg template is HTML-escaped, and so is every render_string source. A comment that says <script>alert(1)</script> arrives on the page as text, which is the only sane default for a variable you did not personally vouch for.

Templates with any other suffix — .txt, .md, .json — are left alone, so a plain-text mail body renders exactly as written.

The escape hatch is markupsafe.Markup, and it exists for markup you built yourself and are willing to stand behind. Two spellings, one meaning:

vouching for markup python
from markupsafe import Markup

def badge(count):
    return Markup(f'<span class="badge">{count}</span>')
the same decision, in the template html
<!-- only ever on a value you produced, never on one a request produced -->
{{ trusted_markup | safe }}
 

The rule that keeps this useful rather than decorative: never wrap a value that came from a request. Markup and | safe are how you say I wrote this HTML, not how you make a stubborn variable render. An existing | e filter keeps working and does not double escape.

Everything the framework hands your templates — src(), theme, frontend(), wf_tailwind — is already Markup, so those keep rendering as markup with no work from you. One footnote worth knowing if you assemble HTML in Python: joining Markup pieces with a plain string separator gives you a plain string back, and a plain string gets escaped. Markup("\n").join(parts) keeps the promise; "\n".join(parts) loses it.

Three sources, in order

When your app starts, the framework assembles the Jinja loader from three layers and searches them in this exact order:

  1. Your app — everything inside fluid/templates, searched first for a plain name.
  2. Additives — each enabled Additive contributes its templates under its own id namespace, so they never shadow your unprefixed names (more on that in the Additives chapters).
  3. The framework — the bundled templates like fluid_base.html and the error pages.

Because your app sits before the framework, an unprefixed name resolves to your file first and falls through to the framework only if you don't have one. So the names map to disk like this:

resolution text
render("index.html")        -> fluid/templates/index.html        (your app)
render("docs/intro.html")   -> fluid/templates/docs/intro.html   (your app)
render("fluid_base.html")   -> bundled with the framework
render("errors/404.html")   -> bundled with the framework
 

The path you pass is always relative to fluid/templates. Nest as deeply as you like: the documentation site you are reading right now renders pages with names such as docs/1.0.0b3/get-started.html, which is just a file at that path inside its fluid/templates.

Overriding framework templates

That ordering has a useful consequence: since your app is searched first, you can replace any framework template by simply creating a file with the same name. Want your own 404 page or your own base layout? Drop it into fluid/templates and it takes over — no configuration needed:

fluid/templates/errors/404.html html
{% extends "fluid_base.html" %}

{% block content %}
    <h1>{{ _('Lost at sea') }}</h1>
{% endblock %}

Explicit namespaces

Sometimes you want to be unambiguous about which layer you mean. Each source also answers to a prefix, so you can skip the fall-through entirely:

  • app/… — force your app's templates, e.g. app/index.html.
  • fluid/… — force the framework's templates, e.g. fluid/fluid_base.html. Handy when you have overridden a name locally but still want to extend the original.
  • <additive_id>/… — an Additive's templates always live under its id.

This is why Additives never collide with your app: their files are only ever reachable through their own id namespace. An Additive's own render() adds that prefix for you automatically, so inside an Additive you keep writing plain names.

What the framework gives you

For reference, these are the templates you can always extend or override:

  • fluid_base.html — the default page layout.
  • base_email.html — a starting point for HTML mails.
  • errors/<status>.html — the default error pages for the common HTTP statuses (400, 401, 403, 404, 405, 429, 500, 502, 503), rendered automatically by the processing layer. In debug mode a detailed errors/debug/500.html takes over for unhandled exceptions.
 

Those pages are only rendered for a client that asks for HTML. Everything else gets JSON, and outside debug mode that JSON says nothing but the status text — the exception's message, its type and its traceback are debug-only, so a production 500 cannot hand an api client the sql fragment or connection string the exception happened to carry.

When the stack is sealed

The loader stack is assembled once, at startup, and then frozen. Additives add theirs while they are being enabled; if you ever need to contribute one yourself, that is what fluid.add_template_loader(loader) is for, and it has to happen before the server comes up.

Freezing buys you the second half: outside debug mode Jinja stops checking whether a template changed on disk, because nothing can. In debug mode it does check, so your edits show up on reload — which is also why a page renders noticeably faster in production than it does while you are writing it.

 

Keep this resolution order in mind when you name your pages. As long as your page lives under fluid/templates and doesn't shadow a framework name you rely on, you are free to structure the directory however suits your project.

Continue reading

From here you can continue straight with the Additives introduction.