Routing

The Quickstart guide shows how to serve a page from / using Blu.

This section explains how to serve pages from other paths.

File-Based Routing

Blu uses file-based routing. This means that the path that a page is served from is based on the file’s location in the app/ directory.

For example, if you want to serve the page in the Quickstart guide from /path/to/page instead of from /, you would move the __index__.py file from app/ to app/path/to/page/:

app/path/to/page/__index__.py
from blu.html import body, head, html


def __page__():
    return html[
        head,
        body['Hello World!'],
    ]

After this change, your project directory would look like this:

app/
  path/
    to/
      page/
        __index__.py

Dynamic Route Segments

To add a dynamic route segment, create a directory whose name is surrounded by single underscores, like the _employee_id_ directory in this example:

app/
  employees/
    _employee_id_/
      __index__.py
app/employees/_employee_id_/__index__.py
from blu.html import p


def __page__():
    return p['This is an employee profile page.']

Now, if you go to /employees/325832, or to /employees/839481, or even to /employees/cheese, you’ll get something like this:

This is an employee profile page.

Dynamic route segments usually aren’t that useful unless there’s actual dyanmic content, so let’s use that employee id from the URL path in our page:

app/employees/_employee_id_/__index__.py
from blu.html import p


def __page__(employee_id):
    return p[f'This is the profile page for employee #{employee_id}.']

Now, visiting /employees/325832 gives us:

This is the profile page for employee #325832.

Notice that the __page__() function now has the argument employee_id. Because of this, Blu looks for a route segment in __index__.py’s URL path that matches the argument name, with single underscores around it, and takes the value of that segment in the actual URL to pass in as that argument.

A route can have multiple dynamic segments:

app/
  employees/
    _employee_id_/
      time_card/
        _date_/
          __index__.py
app/employees/_employee_id_/time_card/_date_/__index__.py
from blu.html import p


def __page__(employee_id, date):
    return p[f'This is employee #{employee_id}\'s time card for {date}.']

In this case, visiting /employees/325832/time_card/2024-12-10 gives us:

This is employee #325832's time card for 2024-12-10.

Default Handlers

You can add a catch-all handler to a route segment that handles a request if the route is matched up to that point but no __index__.py file is on a path that exactly matches the URL. You do this by creating a __default__.py file with a __page__() function:

app/
  foo/
    __default__.py
    bar/
      __index__.py
app/foo/__default__.py
from blu.html import p


def __page__():
    return p['This is the default page.']
app/foo/bar/__index__.py
from blu.html import p


def __page__():
    return p['This is the page for /foo/bar.']

In this example, visiting /foo/bar will give us:

This is the page for /foo/bar.

But visiting /foo or /foo/some/other/path or /foo/bar/some/other/path will give us:

This is the default page.

Accessing the remaining path

To read the remaining, unmatched portion of the URL in a default handler, add an argument that ends in a double underscore (__) to __page__()’s function signature:

app/
  foo/
    __default__.py
app/foo/__default__.py
from blu.html import p


def __page__(path__):
    return p[f'The remaining path is {path__}.']

In this example, visiting /foo/a/b/c gives us:

The remaining path is a/b/c.

This can be mixed with dynamic route segments:

app/
  _my_param_/
    __default__.py
app/_my_param_/__default__.py
from blu.html import b, p


def __page__(my_param, path__):
    return (
        p[
            b['my_param value:'], ' ', my_param,
        ],
        p[
            b['remaining path:'], ' ', path__,
        ],
    )

In this example, visiting /my-param-value/a/b/c gives us:

my_param value: my-param-value

remaining path: a/b/c

Query Parameters

You can also capture query parameters in an __index__.py or __default__.py handler. These must be separated from route parameters with a /:

app/
_foo_/

__index__.py

app/_foo_/__index__.py
from blu.html import b, p


def __page__(foo, /, bar, baz):
    return (
        p[
            b['foo:'], ' ', foo,
        ],
        p[
            b['bar:'], ' ', bar,
        ],
        p[
            b['baz:'], ' ', baz,
        ],
    )

In this example, visiting /A?bar=B&baz=C gives us:

foo: A

bar: B

baz: C

A python function’s arguments cannot start with /. To create a route with query parameters but no route parameters, put a single underscore (_) before the /:

app/
  foo/
    __index__.py
app/foo/__index__.py
from blu.html import b, p


def __page__(_, /, bar, baz):
    return (
        p[
            b['bar:'], ' ', bar,
        ],
        p[
            b['baz:'], ' ', baz,
        ],
    )

In this example, visiting /foo?bar=A&baz=B gives us:

bar: A

baz: B

The __page__() function can also accept query parameters using keyword argument packing:

app/
  foo/
    __index__.py
app/foo/__index__.py
from blu.html import b, p


def __page__(_, /, bar, **kwargs):
    return (
        p[
            b['bar:'], ' ', bar,
        ],
        p[
            b['baz:'], ' ', kwargs['baz'],
        ],
        p[
            b['hello:'], ' ', kwargs['hello']
        ],
    )

In this example, visiting /foo?bar=A&baz=B&hello=C gives us:

bar: A

baz: B

hello: C

Return Values

A __page__() function can return any valid child of an HTML element (see the Children subsection of Creating Pages). The same rules apply for Iterables (again, see the Children subsection of Creating Pages):

from blu import Key
from blu.html import p


# Wrong! All Iterables except strings and tuples must be keyed.
def __page__():
    return p[
        ['A', 'B', 'C'],
    ]

# Right. The list is keyed.
def __page__():
    return p[
        [
            Key(0)['A'],
            Key(1)['B'],
            Key(2)['C'],
        ],
    ]

# Right. Strings don't have to be keyed, even though they are iterable.
def __page__():
    return p['ABC']

# Right. Tuples don't have to be keyed, even though they are iterable.
def __page__():
    return p[
        ('A', 'B', 'C'),
    ]

You can also return a blu.Response to set the status code and/or response headers of the page:

from blu import Response
from blu.html import p


def __page__():
    return Response(
        p['Hello.'],
        status=404,
        headers={
            'Cache-Control': 'no-cache',
            'Last-Modified': 'Tue, 10 Dec 2024 10:00:00 GMT',
        },
    )