Hybrid Static/Dynamic Freeze

Static HTML with live-block placeholders backed by an origin

3 min read 511 words

Overview

chirp freezenormally renders every route to static HTML. Live blocks let you keep 99 % of a page static while declaring named blocks that should render dynamically at request time.

At freeze time the declared block's HTML is replaced with an htmx placeholder pointing at the block-fetch dispatcher (/_frag{path}?_b={block}). The frozen page deploys to any static host; the placeholder fetches the block from an origin when the visitor loads the page.

Declaring a live block

from chirp import App, Fragment, Request

app = App()

@app.route("/docs/{slug:path}")
def docs(slug: str):
    return Page("docs/page.html", slug=slug)

@app.live_block(
    "/docs/{slug:path}",
    "recent_updates",
    trigger="load delay:100ms",
    skeleton="<div class='skel'>Loading…</div>",
)
async def recent_updates(request: Request) -> Fragment:
    slug = request.path_params["slug"]
    updates = await fetch_updates(slug)
    return Fragment("docs/page.html", "recent_updates", updates=updates)

Both arguments are validated at app.check():

  • live_block_unreachable_route— the route isn't registered.
  • live_block_unknown— the template has no block with that name.

Frozen output

Afterchirp freeze, the recent_updatesblock is emitted as:

<div hx-get="/_frag/docs/intro?_b=recent_updates"
     hx-trigger="load delay:100ms"
     hx-swap="innerHTML"
     hx-target="this"
     data-chirp-live="recent_updates"><div class='skel'>Loading…</div></div>

Everything outside that block remains static — no JS is required to read the rest of the page, and search engines index the surrounding content.

What an origin must serve

The origin is any Chirp process mounted at the same URL space as the frozen site. It handles exactly two concerns:

  1. GET /_frag{path}?_b={block} — the block-fetch dispatcher. Registered automatically by Chirp; returns only the named block.
  2. Cache headers — setCache-Control: public, s-maxage=…on dispatcher responses you want your CDN to hold onto. Usecache_seconds= on the decorator to set the value from the declaration site.

If the origin is unreachable, the placeholder stays visible (htmx shows the skeleton content). No JavaScript error is raised — the page still functions, the live block just never resolves. This is the graceful-degradation guarantee: frozen pages never become unusable because the origin is down.

Deployment shapes

Full hybrid (static + live origin)

  • CDN / S3 / GitHub Pages serves/distas usual.
  • A small Chirp worker serves/_frag/**behind the CDN.
  • Route/_frag/*requests to the worker via CDN path rules.

Static-only (no origin)

  • Just runchirp freezewithout declaring any live blocks.
  • Output is byte-identical to pre-live-blocks Chirp (Invariant 1).

Origin-only (no freeze)

  • Normalchirp run— every block resolves server-side.
  • @app.live_blockdeclarations are still valid; they just add no placeholders.

Caveats

  • Live blocks are matched against the rendered HTML by string equality. If the block renders to an empty string (no content), freeze emits a warning and skips rewriting that block.
  • The leaf template's rendered context is used to render the block in isolation during freeze. Side-effectful template context (e.g. a generator that can only be iterated once) is not supported.
  • The block-fetch URL scheme is reserved: user routes starting with/_frag/ raiseConfigurationErrorat freeze time.
  • freeze_paramsvalues must expand to normal URL path segments. Values that introduce. or ..path segments are reported as freeze errors and are not written to disk.