Boosted Navigation

How hx-boost swaps work in Chirp, when they redirect, and the debug warnings that catch silent failures

Page actions AI-ready formats and sharing
Open LLM text
Share with AI
Ask Claude Ask ChatGPT Ask Gemini Ask Copilot

Overview

Boosted navigation turns an ordinary<a href="...">click into an htmx swap. The page never full-reloads: htmx fetches the destination URL, pulls out the matching block, and replaces one DOM node. The shell — topbar, sidebar, footer — stays mounted, and only the content outlet re-renders.

You opt in with htmx'shx-boost="true". Chirp's job is to render the right block on the way back and to fail safely when a link can't swap into the current shell.

When to reach for it

Reach for boosted navigation when links inside a persistent app shell should feel like an SPA without you writinghx-*attributes on every link. Return a [[docs/about/core-concepts/return-values|Page]] and Chirp picks the wide block for a boosted click and the narrow block for an in-page fragment swap automatically.

Request type HX-Boosted Block rendered
Full page load absent full template (composed into the layout)
Boosted link click true the widepage_block_name (e.g. page_root)
Narrow fragment (form post, search input) absent;HX-Request: true the narrowblock_name (e.g. page_content)

Minimal working example

ReturnPage(...)with two block names. The first is the narrow block for in-page fragment swaps;page_block_nameis the wider, fragment-safe root used for a boosted navigation. Chirp reads theHX-Boostedheader and chooses.

def get(request: Request) -> Page:
    query = normalize_query(request.query.get("q"))
    group = normalize_query(request.query.get("group"))
    return Page(
        "contacts/page.html",
        "page_content",
        page_block_name="page_root",
        **page_context(query, group),
    )

Source: examples/chirpui/contacts_shell/pages/contacts/page.py.

That single handler serves three callers: a full page load, a boosted link click, and a narrow in-page swap. See the contacts shell example for the full app.

The contract

A boostedGETis checked against three rules. If any rule can't be met, Chirp emits anHX-Redirectso the browser performs a real navigation — it never renders a broken fragment.

  1. 1

    One block matches `HX-Target`

    The destination route's layout chain must define a block mapped to the requested target id. Chirp normalizes the header once at the request layer, so#mainand mainresolve to the same id.

  2. 2

    The destination is reachable from the current shell

    If the link crosses a shell boundary — say, marketing site to dashboard app — the destination layout isn't mounted, so Chirp redirects instead of swapping.

  3. 3

    The response body is a fragment, not a full page

    A<!DOCTYPE> in a fragment response means the handler bypassed Page(...)or returned the wrongTemplate(...). In debug mode Chirp warns you — see Debug warnings.

Which attributes a link needs depends on whether it stays inside the current shell, crosses into another, or leaves the app.

Link destination Use What you get
Same shell, in-outlet route_link_attrs() (chirp-ui) or a bare <a>inside a boosted region hx-boost="true"plus the inherited target
Crosses shell / layout domain swap_attrs(href) A framework-resolvedhx-target, or an HX-Redirectat request time
External / download / anchor hx-boost="false" Full navigation; htmx leaves the element alone

Inside an app-shell layout (or any boosted outlet),hx-boost="true"is set on the outlet itself. Child<a>tags inherit it, so no per-link markup is needed:

<a href="/contacts">Contacts</a>
<a href="/contacts/42">View</a>

chirp-ui's sidebar_link() emits route_link_attrs()for links outside the content outlet (the sidebar is not inside the boosted region), so those links still participate in swaps.

When the destination lives in a different layout domain — for example, linking from the marketing site root into the dashboard at/app/*— hand the href to swap_attrsand let the framework pick the target:

<a href="/app/inbox" {{ swap_attrs("/app/inbox") | html_attrs }}>Open inbox</a>

swap_attrswalks the current and destination layout chains, finds the nearest shared ancestor outlet, and returns{"hx-target": "#<outlet>", "hx-boost": "true"}. If no shared ancestor exists, the request-time check redirects: the link still works, the browser just does a real navigation.

Opt out withhx-boost="false"

Use this for elements htmx must not touch:

<a href="/export.csv" hx-boost="false" download>Download CSV</a>
<a href="https://example.com" hx-boost="false">External site</a>
<a href="#section" hx-boost="false">Jump to section</a>

Cross-shell redirects

New in 0.5

A boosted click that crosses a shell boundary cannot render into the current outlet, because the destination layout is not mounted. Chirp emits HX-Redirect: <destination>and htmx performs a real navigation:

Scenario: user clicks /app/inbox from /marketing/about
Current shell: marketing (site-content outlet)
Destination:   app dashboard (main outlet)
Result:        HX-Redirect → browser fetches /app/inbox as a full page

Apps without any app shell pass through untouched — there is no shell boundary to cross. You don't configure this branch; the one thing you act on is reading the normalized target viarequest.htmx_target_idin any custom code path.

Debug warnings

WhenAppConfig(debug=True), the DebugFragmentValidatormiddleware inspects buffered fragment responses and logs a warning when it finds patterns that mean a swap is broken. It runs only when the render intent isfragment(or unknownon an htmx request); streaming responses, non-HTML content, and full-page intents pass through. It is on by default (debug_fragment_validator defaults toTrue).

The validator flags two patterns:

Pattern Meaning Fix
<!DOCTYPE>in fragment body A full page rendered into an outlet ReturnPage(...) or Fragment(...), not Template(...)
A registered shell-region id appearing more than once A shell region is duplicated in the fragment Remove the duplicate block; OOB regions own their DOM id (see OOB fail-loud policy)

To silence a warning, fix the handler first — the warning is almost always correct. If your app intentionally renders pre-serialized fragments with repeated ids, opt out globally withAppConfig(debug_fragment_validator=False).

Troubleshooting

Symptom Likely cause
Link does a full reload instead of swapping hx-boost="false" is set on an ancestor, or the <a>is outside the boosted outlet
Fragment appears but the shell jumps The handler returnedTemplate(...) (full page) instead of Page(...); the debug validator warns
Cross-shell click does a full navigation Expected — the destination is in another shell, so Chirp emitsHX-Redirect
Page renders twice / duplicate ids A shell-region block is duplicated in the fragment; the validator warns on registered ids
HX-Target: #main works but HX-Target: mainfails (or the reverse) A custom code path forgot to normalize; userequest.htmx_target_id