Quickstart

Go from an empty directory to a running Chirp app with a live search box in about five minutes.

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

What you'll build

Chirp serves HTML over the wire — full pages for browser navigations and HTML fragments for htmx requests, both from one template. This page goes from an empty directory to a running app with a live search box in about five minutes.

You return values likeTemplate and Page; the return type tells Chirp what to render. If you know Flask, the routing will feel familiar — the fragment loop is the one new idea.

Start a project

You have two ways in. Scaffold a ready-made app withchirp new, or build one by hand to see each piece. Both land on a running app athttp://127.0.0.1:8000.

chirp new myapp
cd myapp
python app.py

The scaffold ships with auth, sessions, CSRF, and security headers already wired. Log in withadmin / password and open http://127.0.0.1:8000/dashboard.

Create a file calledapp.py:

from chirp import App

app = App()

@app.route("/")
def index():
    return "Hello, World!"

app.run()

Run it with python app.py and open http://127.0.0.1:8000. A handler returns a value; a plain string becomes an HTML response.

Add a template

Create atemplates/ directory and add templates/base.html:

<!DOCTYPE html>
<html>
<head><title>{{ title }}</title></head>
<body>
  {% block content %}{% endblock %}
</body>
</html>

Add templates/index.html:

{% extends "base.html" %}

{% block content %}
  <h1>{{ title }}</h1>
  <p>Welcome to my Chirp app.</p>
{% endblock %}

Update app.py:

from chirp import App, Template

app = App()

@app.route("/")
def index():
    return Template("index.html", title="Home")

app.run()

Handlers return values. Template tells Chirp to render index.htmlwith the given context through kida templates.

Render a fragment

This is where Chirp diverges from Flask. A search route can serve a full page to a browser and just the results to an htmx request — from the same template, with no separate partials directory.

Addtemplates/search.html. Wrap the results in a named block so it can be rendered on its own:

{% extends "base.html" %}

{% block content %}
  <h1>Search</h1>
  <input type="search" name="q"
         hx-get="/search" hx-target="#results" hx-trigger="input changed delay:300ms">

  {% block results %}
    <div id="results">
      {% for item in results %}
        <p>{{ item }}</p>
      {% endfor %}
    </div>
  {% endblock %}
{% endblock %}

Update app.py. Return [[docs/about/core-concepts/return-values|Page]] and Chirp negotiates the response: a full page for browser navigations, the named block for narrow htmx swaps.

from chirp import App, Template, Page, Request

app = App()

ITEMS = ["apple", "banana", "cherry", "date", "elderberry"]

@app.route("/")
def index():
    return Template("index.html", title="Home")

@app.route("/search")
def search(request: Request):
    q = request.query.get("q", "")
    results = [i for i in ITEMS if q.lower() in i.lower()] if q else ITEMS
    return Page("search.html", "results", title="Search", results=results)

app.run()

Page replaces the manual if request.is_htmx: return Fragment(...)branch you'd otherwise write on every htmx-reachable route.

Wire up htmx

To make the fragment swap fire, include htmx intemplates/base.html:

<!DOCTYPE html>
<html>
<head>
  <title>{{ title }}</title>
  <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js"></script>
</head>
<body>
  {% block content %}{% endblock %}
</body>
</html>

Now the search input sends hx-get requests to /search, and Chirp responds with only theresultsblock — no full page reload, no separate partials, no hand-written JavaScript.

Stream live updates

For updates that arrive after the page loads — notifications, a ticker, a live feed — use Server-Sent Events. A route returns anEventStream that yields Fragmentswaps over a long-lived connection.

  1. 1

    Open an SSE scope in your template

    Addtemplates/feed.html. Extend the boost layout, wrap the live region in a named block, and declare where the stream connects:

    {% extends "chirp/layouts/boost.html" %}
    {% block content %}
      {% block live_block %}
        <ol id="live_block">
          {% for item in items %}
          <li>{{ item }}</li>
          {% endfor %}
        </ol>
      {% endblock %}
    {% endblock %}
    {% block sse_scope %}
      {% from "chirp/sse.html" import sse_scope %}
      {{ sse_scope("/events", swap="live_block") }}
    {% endblock %}
    
  2. 2

    Stream fragments from the route

    EventStream takes an async generator. Each yieldre-renders the live_block from feed.htmland pushes it down the connection:

    from chirp import EventStream, Fragment
    
    @app.route("/events", referenced=True)
    async def events():
        async def stream():
            yield Fragment("feed.html", "live_block", items=ITEMS)
        return EventStream(stream())
    
  3. 3

    Run chirp check

    chirp check myapp:app
    

    This catches SSE scope violations and route/template mismatches before you open the browser.

See View Transitions + OOB for the full real-time pattern.

Next steps

You now have the fragment loop running. The natural next step is to build the same loop with a form, tests, andchirp checkend to end: