Overview
You added a shell region — breadcrumbs, a sidebar, a document title — and on boosted navigation it either raisesBlockNotFoundError or silently vanishes from the page. This page shows you how to register an out-of-band (OOB) region, when to mark it optional, and how to read the error.
The OOB registry is your app's map from a template block name (likebreadcrumbs_oob) to the DOM element it swaps into. Chirp checks that map at startup and again at render time, so a missing region fails loudly instead of wiping live DOM content.
Register a region
Callapp.register_oob_region() during setup, before app.run():
app.register_oob_region(
"breadcrumbs_oob",
target_id="chirpui-topbar-breadcrumbs",
swap="innerHTML",
)
The three kwargs you set in practice:
| Kwarg | Default | What it does |
|---|---|---|
target_id |
required | The DOM id the OOB fragment updates. |
swap |
"innerHTML" |
The htmx swap strategy.innerHTML and outerHTMLare the common ones; the validator accepts any htmx strategy plus modifiers. |
optional |
False |
WhenTrue, layouts may legitimately omit this block. See below. |
Defining the matching block in your layout is part of the [[docs/build-apps/ui-extensions/app-shell|app-shell{% region %} pattern]] — one definition serves both full-page renders and OOB swaps. Plain {% block name %}...{% endblock %}works too when you don't need the dual output.
FixBlockNotFoundError
The error names the block, the template, and the target DOM id. Work down these in order — the first one is the right fix the vast majority of the time.
- 1
Add the block to the layout
The registry says you promised this region exists. Add it to the layout template the error names:
{% region breadcrumbs_oob(breadcrumb_items=[]) %} {% if breadcrumb_items %} <nav aria-label="breadcrumb">...</nav> {% end %} {% end %}This is the fix when the cause is a missing layout region or a typo in the block name.
- 2
Mark the region optional
If the region is genuinely absent from some layouts by design — a shell concern defined in a framework-level layout that custom routes skip — pass
optional=True. The render path then drops the region instead of raising. See when to mark a region optional. - 3
Remove the registration
If nothing uses the region anymore, delete the
register_oob_regioncall. A startup WARNING about an optional orphan is the signal that a registration has gone stale.
When to mark a region optional
Mark a regionoptional=True only when it is expected to be absent from some layouts. The canonical case is chirp-ui's shell regions: an app using the full app-shell layout defines breadcrumbs_oob and sidebar_oob, but a bare custom layout may not — and that is fine.
app.register_oob_region(
"breadcrumbs_oob",
target_id="chirpui-topbar-breadcrumbs",
optional=True, # shell region; bare custom layouts may omit it
)
Catch it before users do
app.check() runs the oob_registrycontract check at startup. For each registered block it walks every layout template and confirms at least one defines a matching block. An orphaned non-optional registration is an ERROR, so gate CI on it and a missing region fails the build, not production.
The canonical CI gate is the CLI —chirp check loads your app, runs every contract check, and exits non-zero on any ERROR (add --warnings-as-errorsto fail on optional-orphan WARNINGs too):
chirp check myapp:app --warnings-as-errors
To assert the same thing from a pytest suite, call app.check() on a frozen app. It prints the report and raises SystemExit(1) on any ERROR (it returns None— it does not hand back a list of issues), so the test passes only when the contract is clean:
import pytest
def test_app_contracts():
app = make_app() # build and freeze your app
try:
app.check()
except SystemExit as exc:
pytest.fail(f"app.check() reported contract errors (exit {exc.code})")
How severity is decided
The check classifies each orphaned registration by whether it was marked optional:
| Registered as | Layout defines block? | Severity | Meaning |
|---|---|---|---|
optional=False |
yes | — | OK |
optional=False |
no | ERROR | Render would raiseBlockNotFoundError |
optional=True |
yes | — | OK |
optional=True |
no | WARNING | Render skips the region; the registration may be stale |
oob_registryis a contract category like any other, so you can override its severity globally — for example to keep the older permissive behavior during a migration:
from chirp.contracts.types import Severity
app.override_contract_severity("oob_registry", Severity.WARNING)
This is a migration escape hatch, not a supported long-term setting. See contract categories and severities for the full list.
What `BlockNotFoundError` carries
BlockNotFoundError multi-inherits from ChirpError and KeyError, so existing except KeyError handlers (including Kida's render_blockcontract) still catch it. The instance carries:
.template— the layout template that was missing the block.block— the block name that wasn't found.region— the target DOM id, orNone
See error types for the full hierarchy.
What changed in 0.5
Changed in 0.5Earlier versions silently swallowed a region update whose block didn't exist: the region was emitted withhtml="", which wipes the target element's DOM content on every boosted navigation. Bugs like "my breadcrumbs keep disappearing" were impossible to trace from the server. Fail-loud rendering surfaces them at startup (the contract check) and at render time (the exception) instead.