Template system
SPLENT extends Flask’s Jinja2 engine with two mechanisms: context processors that inject variables into every template automatically, and template hooks that let features contribute HTML fragments to named slots in layouts.
Table of contents
- JinjaManager
- inject_context_vars
- Template hooks
- Product layout hooks
- Frontend asset loading
- Summary: variables available in every template
- See also
JinjaManager
File: managers/jinja_manager.py
Initialized in create_splent_app() as step 7 of the pipeline, after all managers but before features.
What it does
-
Registers a context processor — merges the base context dict with every feature’s
inject_context_vars()return value. The result is available in every Jinja template without passing anything torender_template. -
Registers Jinja globals — exposes
get_template_hooks(name)as a global function callable from any template.
Context processor pipeline
On every request, Flask calls all registered context processors to build the template context. JinjaManager registers one that:
- Starts with the base context passed at construction (e.g.,
{"SPLENT_APP": "sample_splent_app"}) - Iterates over all registered feature context processors
- Calls each feature’s
inject_context_vars(app)and merges the returned dict - Returns the merged dict
If a feature’s inject_context_vars raises an exception or returns a non-dict, a warning is logged and that feature’s contribution is skipped — it does not crash the request.
inject_context_vars
Defined in: each feature’s __init__.py
def inject_context_vars(app):
return {
"is_redis_enabled": True,
"app_version": "1.1.0",
}
The dict returned here becomes available in every Jinja template in the application, not just templates from that feature. Use it for:
- Feature flags (
auth_enabled,profile_enabled) - Configuration values that templates need (
mail_sender,app_name) - Current user utilities
- Any computed value that is expensive to re-pass on every
render_templatecall
Naming collisions
If two features return the same key, the last feature in the topological load order wins. Prefix your keys with the feature name to avoid collisions:
# Risky — "enabled" is too generic
return {"enabled": True}
# Safe
return {"notes_enabled": True}
Access in templates
<!-- No argument needed in render_template — available automatically -->
{% if auth_enabled %}
<a href="{{ url_for('splent_feature_auth.login') }}">Login</a>
{% endif %}
Template hooks
File: hooks/template_hooks.py
Template hooks are named slots in layouts that features can contribute HTML to. They let features inject navigation items, scripts, styles, or any other HTML into any layout without modifying the layout file.
How it works
Feature A registers a hook → "nav_items"
Feature B registers a hook → "nav_items"
Layout template calls: get_template_hooks("nav_items")
→ returns [result_of_A, result_of_B]
→ template renders each one
Hook registry API
| Function | Description |
|---|---|
register_template_hook(name, func) |
Add func to the hook list for slot name |
replace_template_hook(name, func) |
Replace all existing callbacks for slot name with func |
get_template_hooks(name) |
Return the list of hook results for slot name |
clear_hooks() |
Reset the entire registry (used in tests) |
The registry is a module-level dict and is not thread-safe for concurrent writes. Populate hooks only during app startup (inside init_feature), never during request handling.
Registering a hook
In a feature’s init_feature(app) or hooks.py:
from splent_framework.hooks.template_hooks import register_template_hook
def init_feature(app):
register_template_hook("nav_items", render_nav_item)
def render_nav_item():
return '<li><a href="/notes">Notes</a></li>'
register_template_hook(name, func) adds func to the list for slot name.
Replacing a hook
Refinement features can replace an existing hook’s callbacks entirely using replace_template_hook:
from splent_framework.hooks.template_hooks import replace_template_hook
def init_feature(app):
replace_template_hook("nav_items", custom_nav_item)
def custom_nav_item():
return '<li><a href="/custom-notes">Custom Notes</a></li>'
replace_template_hook(name, func) removes all previously registered callbacks for that slot and registers func as the sole callback. This is how refinement features override behavior from base features.
Calling hooks in a template
<!-- base.html or any layout -->
<ul class="nav">
{% for item in get_template_hooks("nav_items") %}
{{ item | safe }}
{% endfor %}
</ul>
get_template_hooks(name) is injected as a Jinja global by JinjaManager. It returns the list of return values from all functions registered under name, in registration order (which follows the topological feature load order).
Product layout hooks
The product’s base_template.html defines 8 standard hook slots. Features fill these slots to contribute UI fragments to the shared layout:
| Hook name | Purpose |
|---|---|
layout.head.css |
Extra stylesheets in <head> |
layout.anonymous_sidebar |
Sidebar items shown to unauthenticated users |
layout.authenticated_sidebar |
Sidebar items shown to authenticated users |
layout.sidebar.authenticated.items |
Individual menu items inside the authenticated sidebar |
layout.navbar.anonymous |
Navbar content for unauthenticated users |
layout.navbar.authenticated |
Navbar content for authenticated users |
layout.footer |
Footer content |
layout.scripts |
Extra <script> tags before </body> |
How features register hook callbacks
A feature registers callbacks during init_feature(app) or in its hooks.py:
from splent_framework.hooks.template_hooks import register_template_hook
def init_feature(app):
register_template_hook("layout.anonymous_sidebar", render_login_link)
register_template_hook("layout.scripts", render_auth_scripts)
def render_login_link():
return '<li><a href="/login">Login</a></li>'
def render_auth_scripts():
return '<script src="/static/auth/auth.bundle.js"></script>'
Feature-defined hook slots
Features are not limited to the product layout hooks listed above. Any feature can define its own hook slots in its templates, and other features can register callbacks for them. This is how optional features extend each other without coupling.
Example: auth exposes auth.login.form_footer
The auth feature’s login template renders a hook slot:
<!-- splent_feature_auth/templates/auth/login_form.html -->
<form method="post">
...
<button type="submit">Login</button>
{% for hook in get_template_hooks("auth.login.form_footer") %}
{{ hook() | safe }}
{% endfor %}
</form>
Example: reset hooks into auth’s slot
The reset feature registers a “Forgot your password?” link into that slot:
# splent_feature_reset/hooks.py
from splent_framework.hooks.template_hooks import register_template_hook
from flask import render_template
def inject_reset_link():
return render_template("hooks/reset_link.html")
register_template_hook("auth.login.form_footer", inject_reset_link)
register_template_hook("auth.signup.form_footer", inject_reset_link)
If splent_feature_reset is not installed, the hook slot renders nothing — auth works normally without the link. If reset is installed, the link appears automatically.
This pattern works because:
- Auth does not import or reference reset in any way.
- Reset depends on auth (declared in the UVL), so it loads after auth and its hooks register into auth’s slots.
- Removing reset from the product removes the link with no code changes.
Naming convention: feature-defined hook slots use the pattern <feature_name>.<template>.<slot> (e.g., auth.login.form_footer). This avoids collisions with the layout.* namespace used by the product.
How refinement features replace hooks
A refinement feature that depends on a base feature can replace its hook output. For example, a premium_auth feature that refines auth:
from splent_framework.hooks.template_hooks import replace_template_hook
def init_feature(app):
replace_template_hook("layout.anonymous_sidebar", render_premium_login)
def render_premium_login():
return '<li><a href="/premium-login">Premium Login</a></li>'
Because features load in topological order, the refinement feature always loads after the base feature, so replace_template_hook is guaranteed to see the existing registration.
Frontend asset loading
Features include their compiled JS bundles via the layout.scripts template hook, not by overriding block-scripts tags in templates.
Old pattern (avoid)
Previously, features loaded scripts by hardcoding asset references in their own templates:
<!-- feature template — creates coupling -->
{% block scripts %}
<script src="{{ url_for('auth.assets', filename='dist/auth.bundle.js') }}"></script>
<script src="{{ url_for('profile.assets', filename='dist/profile.bundle.js') }}"></script>
{% endblock %}
This created invisible frontend coupling: one feature’s template referenced another feature’s assets. Removing profile from the product would break auth’s template.
New pattern: layout.scripts hook
Each feature registers its own bundle in hooks.py:
# splent_feature_auth/hooks.py
from splent_framework.hooks.template_hooks import register_template_hook
from flask import render_template
def auth_scripts():
return render_template("hooks/scripts.html")
register_template_hook("layout.scripts", auth_scripts)
The product’s base_template.html renders all registered scripts automatically:
{% for hook in get_template_hooks("layout.scripts") %}
{{ hook() | safe }}
{% endfor %}
Convention
Features never reference another feature’s assets directly. Each feature registers only its own bundle through layout.scripts. When a feature is added or removed from the product, its scripts appear or disappear automatically — no template edits required.
Summary: variables available in every template
By default, every SPLENT template has access to:
| Variable | Type | Source |
|---|---|---|
SPLENT_APP |
str |
Base context in create_splent_app() |
get_template_hooks(name) |
function |
JinjaManager global |
Everything returned by each feature’s inject_context_vars(app) |
varies | Feature context processors |
| Standard Flask globals | varies | request, session, g, url_for, config, … |
See also
- Feature loading — when
inject_context_varsis collected - Application initialization — where
JinjaManageris called - Architecture layers —
BaseBlueprintfor template folder resolution - Extensibility — how refinement features extend and override base features
feature:hooks— list all hooks registered by a featurefeature:hook:add— add a new hook to a featurefeature:hook:remove— remove a hook from a feature