Navigation registry
How a content feature contributes one entry to the public main navigation, so the menu is composed from the installed features instead of hardcoded per product.
Table of contents
- What it is
- SPL rationale: the menu is derived, not configured
- The
register_nav_itemAPI - How the theme composes the menu
- The admin Menus editor
- Legacy fallback:
SITE_NAV - See also
What it is
The navigation registry is a small framework module —
splent_framework.nav.nav_registry — that lets each content feature declare
one entry in the public main navigation from inside its init_feature(app).
A feature that owns a public-facing page (a blog, a projects listing, a team
page) calls register_nav_item(...) when it loads. The theme then reads every
declared entry and renders the main menu from them.
Unlike template hooks, which inject HTML into a layout slot, a nav entry is a
plain piece of structured data (key, label, href, order, icon) that the
theme is free to reorder, relabel, hide, or merge with custom links.
SPL rationale: the menu is derived, not configured
This is the key point. In a Software Product Line, the main menu is a point of variability, and that variability is resolved at derivation time from the set of installed features:
- Install a content feature → its entry appears in the menu.
- Remove it and re-derive the product → its entry disappears.
The product no longer carries a hand-written list of menu links. Two products built from different feature selections get different menus for free, because each menu is exactly the union of the entries its installed features registered. Adding a page to a product is the same action as adding the feature that owns it.
The register_nav_item API
Import it from splent_framework.nav.nav_registry and call it inside
init_feature(app):
from splent_framework.blueprints.base_blueprint import create_blueprint
from splent_framework.nav.nav_registry import register_nav_item
post_bp = create_blueprint(__name__)
def init_feature(app):
register_nav_item(key="post", label="Blog", href="/blog", order=50)
def inject_context_vars(app):
return {}
| Parameter | Required | Description |
|---|---|---|
key |
yes | Stable identifier for the entry, unique per feature (e.g. "post"). Used to dedupe and to reconcile against runtime overrides. |
label |
yes | Text shown in the menu (e.g. "Blog"). |
href |
yes | Target URL or path (e.g. "/blog"). |
order |
no | Sort weight; lower comes first. Defaults to 100. |
icon |
no | Optional icon identifier for themes that render icons. Defaults to None. |
The call is idempotent by key: registering the same key twice does not
create a duplicate entry.
Companion functions:
get_nav_items()— returns all declared entries, sorted by(order, label).clear_nav_items()— empties the registry.
You rarely call these two yourself. The framework’s FeatureManager calls
clear_nav_items() before loading features in
register_features(), so the registry only ever holds the entries for this
product’s installed features; the theme calls get_nav_items() at render time.
Other content features register their entries the same way — for example
projects, tools, team, and events each call register_nav_item(...) from
their own init_feature.
How the theme composes the menu
A declared entry is the base menu. On top of it sits an optional override
that the admin can edit at runtime. The theme’s nav.py reconciles the two with
compose_nav() every time a page renders:
- Start from the base entries returned by
get_nav_items(). - Load the override JSON from the settings store under the key
site_nav. - Drop any override entry whose feature is no longer installed (its key is gone from the base).
- Append any newly-installed feature whose entry is not yet in the override.
- Apply
_safe_hrefto custom links so external/relative URLs are well-formed. - Dedupe by
key.
This gives two levels of variability:
| Level | When | What it controls |
|---|---|---|
| Derivation | product:derive |
Which features registered an entry — the base menu. |
| Runtime | admin Menus editor | Reorder, hide, or relabel feature entries, and add custom external links. |
Because reconciliation runs at render time, an override can never go stale: if a feature is removed and the product is re-derived, its entry simply disappears from the rendered menu even if it still lingers in the stored override.
The admin Menus editor
The runtime override is produced by the Menus editor shipped in
splent_feature_theme at /admin/menus. The editor lets an administrator:
- reorder feature entries,
- hide entries they don’t want shown,
- relabel entries, and
- add custom external links.
It saves the result as an override JSON document in the settings store under the
site_nav key. That document is exactly what compose_nav() reconciles against
the base on every request — it never replaces the base, it layers on top of it.
Legacy fallback: SITE_NAV
If no installed feature registered a nav entry, the theme falls back to the
product-level config app.config["SITE_NAV"] (the legacy site.nav list — see
Theme system). This keeps older products rendering a
menu during migration.
New products no longer set SITE_NAV: they let the menu derive from their
installed content features and, optionally, refine it through the Menus editor.
See also
hooks.py& template hooks — inject HTML into layout slots (the other way a feature contributes UI)- Theme system — how the public shell consumes
site.*and theSITE_NAVfallback - Feature loading — when
init_featureruns and the registry is cleared