Template hooks

Named insertion points where features inject HTML at runtime. A feature registers a callback, the template renders it — no file modifications needed.

Table of contents

Two levels

Level Declared in Example
Product hooks Product’s pyproject.toml + base_template.html layout.navbar.authenticated
Inter-feature hooks A feature’s own templates notes.create.form_extra

Both use the same API. The only difference is where the slot lives.


Product layout hooks

The product’s base_template.html declares hook slots. Features register callbacks for these slots.

In the product’s pyproject.toml:

[tool.splent.layout]
base_template = "base_template.html"
hooks = [
    "layout.head.css",
    "layout.anonymous_sidebar",
    "layout.authenticated_sidebar",
    "layout.sidebar.authenticated.items",
    "layout.navbar.anonymous",
    "layout.navbar.authenticated",
    "layout.footer",
    "layout.scripts",
]

The 8 standard hooks

Hook Location in layout Typical use
layout.head.css <head> Feature-specific CSS
layout.anonymous_sidebar Sidebar (logged out) Login/signup links
layout.authenticated_sidebar Sidebar (logged in) Logout link
layout.sidebar.authenticated.items Sidebar (logged in, below main items) Edit profile, settings
layout.navbar.anonymous Navbar (logged out) Sign in button
layout.navbar.authenticated Navbar (logged in) User name, avatar
layout.footer Footer Footer links
layout.scripts Before </body> Feature-specific JS

How the template renders hooks

{% for hook in get_template_hooks("layout.navbar.authenticated") %}
    {{ hook() | safe }}
{% endfor %}

JinjaManager registers get_template_hooks as a Jinja global during app initialization.

Registering a hook

# splent_feature_auth/hooks.py
from splent_framework.hooks.template_hooks import register_template_hook
from flask import render_template

def navbar_anonymous():
    return render_template("hooks/navbar_anonymous.html")

register_template_hook("layout.navbar.anonymous", navbar_anonymous)

Multiple features, same hook

Hooks are additive. Both auth and profile can register for layout.navbar.authenticated. Both execute in feature load order (UVL-determined). The HTML is concatenated.


Inter-feature hooks

Product hooks live in the product layout. Inter-feature hooks live in a feature’s own templates — they let other features inject UI into a base feature.

The pattern

  1. Base feature declares a slot in its template:
{% for hook in get_template_hooks("notes.create.form_extra") %}
    {{ hook() | safe }}
{% endfor %}
  1. Refinement feature registers a callback:
register_template_hook("notes.create.form_extra", tags_form_field)

If no feature registers for a slot, it renders nothing.

Passing context to hooks

Hook callbacks can receive arguments:

{% for note in notes %}
    <h5>{{ note.title }}</h5>
    {% for hook in get_template_hooks("notes.index.note_extra") %}
        {{ hook(note) | safe }}
    {% endfor %}
{% endfor %}
def note_tag_badges(note):
    tags = note.get_tags_list()
    if not tags:
        return ""
    return render_template("hooks/note_tag_badges.html", tags=tags)

register_template_hook("notes.index.note_extra", note_tag_badges)

Making form hooks work end-to-end

When a hook injects a form field, the base feature’s route passes extra data through:

FORM_BASE_FIELDS = {"title", "content"}

@notes_bp.route("/notes/create", methods=["GET", "POST"])
@login_required
def create():
    if request.method == "POST":
        title = request.form.get("title", "").strip()
        content = request.form.get("content", "").strip()
        extra = {k: v for k, v in request.form.items() if k not in FORM_BASE_FIELDS}
        notes_service.create(user_id=current_user.id, title=title, content=content, **extra)

The base route doesn’t know about tags — it just passes extra form data through. Remove the refinement and **extra is empty.

Bundle registration via layout.scripts

Features register their compiled JS bundles through the layout.scripts hook:

def auth_scripts():
    return render_template("hooks/scripts.html")

register_template_hook("layout.scripts", auth_scripts)

Adding or removing a feature automatically adds or removes its JS — no manual edits needed.

Hook naming convention

Use dot-separated names: feature.view.slot.

Hook name Where it renders
notes.index.before_list Above the notes list
notes.create.form_extra Extra fields in the create form
auth.login.form_footer Below the login form
layout.sidebar.authenticated.items Product sidebar items

Product-level hooks start with layout.. Feature-level hooks start with the feature’s short name.

Hook replacement

Function Behavior Use case
register_template_hook(name, func) Additive — appends Multiple features contribute
replace_template_hook(name, func) Exclusive — replaces all Refiner replaces entirely

Use replace_template_hook sparingly. It removes all prior registrations for that slot.


See also


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es