Layouts

Layouts give pages a shared skeleton: the html head, navigation, theme, and footer live in a layout once, not in every page.

How layouts work#

A layout.py file exports a layout() function that receives the rendered children and returns the wrapping HTML. The root layout lives at app/layout.py and wraps every page in the project.

def layout(children, request):
    ...

The children argument is the page, already wrapped by any deeper layouts. request is provided when the request is available; the build step may call layouts without a request, so the argument can be omitted from the signature when you do not need it.

The root layout#

The scaffold generates app/layout.py with the document skeleton:

# app/layout.py
from koyoapp.html import body, head, html, link, meta, script
from koyoapp.meta import metadata_tags

from components.site import site_footer

def layout(children, request):
    return html(lang="en")[
        head()[
            meta(charset="utf-8"),
            meta(name="viewport", content="width=device-width, initial-scale=1"),
            meta(name="theme-color", content="#1800ad"),
            link(rel="stylesheet", href="/styles/koyo.css"),
            script(src="/htmx.min.js"),
            metadata_tags(),
        ],
        body(class_="bg-white antialiased")[
            children,
            site_footer(),
        ],
    ]

The root layout loads the compiled stylesheet, the vendored htmx script, and renders the site footer around children. metadata_tags() turns the metadata dict into real head tags.

Nested layouts#

A layout.py in any folder wraps every page below it in the tree, composed inside the root layout. For example, a blog layout:

# app/blog/layout.py
from koyoapp.html import aside, div, h2

def layout(children):
    return div(class_="grid grid-cols-1 lg:grid-cols-4 gap-8")[
        aside(class_="lg:col-span-1")[
            h2("Blog index"),
        ],
        div(class_="lg:col-span-3")[
            children,
        ],
    ]

Here app/blog/page.py and app/blog/[slug]/page.py both render inside the blog sidebar, which itself renders inside the root layout. Nesting orders from the deepest layout outward: page, then folder layouts, then the root.

Using the request#

A layout can accept request to render request-specific content, for example the route that was visited:

def layout(children, request):
    return html(lang="en")[
        body[
            main[
                children,
                p(f"Served via {request.method}"),
            ],
        ],
    ]

Guard any request usage, because during koyoapp build the request is not available. Components used in layouts should treat request as optional and fall back gracefully, the way use_state returns its initial value without a live request.