一键导入
chirp-app-shell-oob
Build app shells with AST-driven OOB updates for sidebar, breadcrumbs, and title. Use when building Chirp + ChirpUI apps with HTMX navigation.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Build app shells with AST-driven OOB updates for sidebar, breadcrumbs, and title. Use when building Chirp + ChirpUI apps with HTMX navigation.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | chirp-app-shell-oob |
| description | Build app shells with AST-driven OOB updates for sidebar, breadcrumbs, and title. Use when building Chirp + ChirpUI apps with HTMX navigation. |
Pattern for building HTMX-navigable apps where sidebar active state, breadcrumbs, and document title update automatically on boosted navigation — driven by Kida's AST metadata, not hard-coded logic.
breadcrumbs_oob, sidebar_oob, title_oob) alongside the full app shellcurrent_path, page_title, and breadcrumb_items in their contexttemplate_metadata() to discover OOB regions at build timehx-boost), Chirp renders the page fragment AND the OOB regions as hx-swap-oob updatesKida's {% region %} construct compiles to BOTH a block (for render_block) AND a callable (for {{ name(args) }}). Use regions instead of separate blocks + defs:
render_block(){% region sidebar_oob(current_path="/") %}...{% end %}{{ sidebar_oob(current_path=current_path | default("/")) }} in app shell slotsrender_block("sidebar_oob", current_path=...) for OOB updates{# target: body #}
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... head content ... -->
<title id="chirpui-document-title">{{ page_title | default("My App") }}</title>
</head>
<body>
{% from "chirpui/app_shell.html" import app_shell %}
{% from "chirpui/sidebar.html" import sidebar, sidebar_section, sidebar_link %}
{% from "chirpui/breadcrumbs.html" import breadcrumbs %}
{% from "chirpui/theme_toggle.html" import theme_toggle %}
{# Regions: one definition for both app shell slots AND OOB render_block #}
{% region breadcrumbs_oob(breadcrumb_items=[{"label":"Home","href":"/"}]) %}
{{ breadcrumbs(breadcrumb_items) }}
{% end %}
{% region title_oob(page_title="My App") %}
<title id="chirpui-document-title" hx-swap-oob="true">{{ page_title }}</title>
{% end %}
{% region sidebar_oob(current_path="/") %}
{% call sidebar() %}
{% call sidebar_section("Pages") %}
{{ sidebar_link("/", "Home", icon="home", active=current_path == "/") }}
{{ sidebar_link("/settings", "Settings", icon="settings", active=current_path.startswith("/settings")) }}
{% end %}
{% end %}
{% end %}
{% call app_shell(brand="My App", sidebar_collapsible=true) %}
{% slot topbar %}
{{ breadcrumbs_oob(breadcrumb_items=breadcrumb_items | default([{"label":"Home","href":"/"}])) }}
{% end %}
{% slot topbar_end %}
{{ theme_toggle() }}
{% end %}
{% slot sidebar %}
{{ sidebar_oob(current_path=current_path | default("/")) }}
{% end %}
{% block content %}{% end %}
{% end %}
</body>
</html>
{# target: body #}?The target annotation tells Chirp where HTMX should swap content. With body, the entire <body> is the swap target for full page loads, and the OOB regions update specific DOM areas during fragment navigation. Using main would skip OOB region rendering entirely.
{# outlet: main #} with app_shell_layoutIf the root layout {% extends "chirpui/app_shell_layout.html" %} (filesystem mount_pages), add {# outlet: main #} below {# target: body #}. Chirp’s LayoutChain matches outlet so HX-Target: #main resolves to this layout and render_with_layouts returns full shell HTML including #page-content (required for hx-select on boosted nav). Without outlet, boosted responses can be bare page HTML and shell swaps break silently.
Every handler must provide three context keys for OOB updates to work:
from chirp import Request
from chirp.templating.composition import PageComposition
def get(request: Request) -> PageComposition:
return PageComposition(
template="settings/page.html",
context={
"current_path": request.path,
"page_title": "Settings",
"breadcrumb_items": [
{"label": "Home", "href": "/"},
{"label": "Settings"},
],
# ... page-specific context ...
},
)
With ChirpUI, fragment_block and page_block are optional — the FragmentTargetRegistry resolves from HX-Target (#main, #page-root, #page-content-inner).
| Key | Purpose | Used by |
|---|---|---|
current_path | Sidebar active state detection | sidebar_oob block |
page_title | Document <title> update | title_oob block |
breadcrumb_items | Breadcrumb trail | breadcrumbs_oob block |
For dynamic pages, use the entity name:
def get(request: Request, skill: Skill) -> PageComposition:
return PageComposition(
template="skill/{name}/page.html",
context={
"current_path": request.path,
"page_title": skill.name,
"breadcrumb_items": [
{"label": "Home", "href": "/"},
{"label": "Skills", "href": "/skills"},
{"label": skill.name},
],
},
)
Page templates define two nested blocks and must render id="page-root" on a wrapper element. app_shell_layout sets hx-select="#page-content" on #main, and custom app_shell layouts should wrap routed content with shell_outlet() to create the same #page-content boundary. page_root remains the broader page-level fragment target inside that shell outlet. A block named page_root does not create the id by itself — HTMX #page-root swaps would match nothing.
{% block page_root %}
<div id="page-root">
{% block page_content %}
<div style="padding: 2rem;">
<h1>Settings</h1>
<p>Page content here.</p>
</div>
{% end %}
</div>
{% end %}
page_root — outer block for full page composition and sidebar nav (hx-target="#main")page_root_inner — inner block for tab clicks (hx-target="#page-root")page_content — narrow block for content-only swaps (hx-target="#page-content-inner")ChirpUI registers these via use_chirp_ui(). main and page-root have triggers_shell_update=True so shell_actions (topbar, breadcrumbs, title) update on sidebar and tab clicks; page-content-inner has triggers_shell_update=False so narrow swaps don't update the shell. Custom targets: app.register_fragment_target("id", fragment_block="...", triggers_shell_update=True).
Use _context.py files for section-level shared context:
# pages/settings/_context.py
def context() -> dict:
return {
"page_title": "Settings",
"breadcrumb_items": [
{"label": "Home", "href": "/"},
{"label": "Settings"},
],
}
Child pages override by providing the same keys in their handler context.
Chirp does not hard-code which blocks to render as OOB. Instead, it uses Kida's template_metadata():
build_layout_contract() calls template_metadata() on the root layout*_oob are identified as OOB candidatescache_scope and depends_on metadata (from Kida's AST) determines rendering behavior:
cache_scope: "site" → skip (static, doesn't change per page)depends_on: {"page_title"} → skip if page_title not in contextLayoutContract is cached per template for efficiencyThis means adding new OOB regions requires only:
{% region new_region_oob(params) %}...{% end %} to the layout_OOB_TARGET_MAP{# target: body #} (not main)app_shell_layout via mount_pages: also {# outlet: main #} for boosted #main / #page-content{% region name_oob(params) %}...{% end %} for OOB regions (zero duplication){{ name_oob(param=value | default(...)) }}{% from %} imports are at template top level (compile-time, works everywhere)current_path, page_title, breadcrumb_itemspage_root / page_content two-block patterncurrent_path) are used directly in the region body