Refactoring page titles

Current page titles are simple <h1> elements, defaulting to {{title}}, which take space and do the job. The rendering we have is also often a bit off, with page content too close to the <h1> titles.

We currently do not have a standard way to link to pages “above” when there is a clear hierarchy: for example, a worker view is logically below a worker list view, or a worker pool view; an artifact is in a workspace; a collection item is in a collection; and so on.

I notice that GitLab tends not to have page titles in several pages, using breadcrumbs to provide context instead.

A basic refactoring proposal

Describing a page visually

All pages currently in debusine use a string for title except for artifact views, which also have an icon.

Pages could also distinguish from a shorter title and a more detailed description, depending on layout tradeoffs between size and detail.

A page could then be described, with all attributes needed to represent it visually, by a structure like this:

class PageInfo(NamedTuple):
    title: str
    long_title: str | None = None
    url: str | None = None
    icon: str | None = None

Move titles to the base template

A simple first step is to move <h1> elements to the base template. This makes individual page templates simpler, and allows to fix the spacing by intervening in the base template.

It also allows tweaking rendering in alternative page layouts: for example _base_rightbar.html may decide whether to have the title span the whole page or only the main content.

We can extend BaseUIView.get_title to return str | PageInfo to allow it to also provide an icon where appropriate.

Introduce the concept of parent pages

We can have the base template generate a static hierarchy of breadcrumbs from a list of PageInfo records.

To do so, we can extend BaseUIView.get_title to return str | list[PageInfo], with one entry per parent, and the last entry representing the current page.

This can be used by views to provide parent links. It can also be implemented by intermediate view mixins: for example, WorkspaceView can return a link to the workspace detail view as the default top parent of all views inheriting from it.

Pages that may have multiple parents

Artifact views are an example of views that can appear in workspaces, in collections or in work requests, meaning that their generated parent lists have 2 in 3 chances to lose context in navigation.

This can be worked around by making artifact views appear multiple times in the URL namespace, and generate different parent lists depending on where they are.

Experiment with layouts

At this point the base template has all the information it needs to render breadcrumbs and titles, and we are then free to experiment with doing away from <h1> elements altogether, incorporating context into breadcrumbs, or with providing breadcrumbs and visible page titles, as we see fit and as site usage evolves.

There is enough information to be creative: for example, short titles could be used for all steps except the last one, which can use the long title to give more details about the current page. Some pages may indeed benefit from retaining a big title.

A more comprehensive proposal

A more comprehensive version of the above is to introduce the concept of a Place:

class Place:
    """Describe a page in Debusine's web space."""

    #: Short title
    title: str
    #: Long title (defaulting to short)
    long_title: str | None = None
    #: URL
    url: str | None = None
    #: Icon
    icon: str | None = None

We can create collections of Place factories as we have for sidebar and ui_shortcut:

debusine.web.views.places:

def create_workspace(workspace: Workspace) -> Place:
    return Place(
        workspace.name, f"{workspace.name} workspace",
        workspace.get_absolute_url(), Icons.ICON_WORKSPACE
    )

Place objects can then be used by BaseUIView.get_title as well as by ui shortcuts and sidebar items.

We can experiment with ways for Place objects to render themselves, either with methods similar to Django’s Form, (as_a, as_a_long, …) or using the Debusine widget template, possibly augmented with a parameter to select rendering variants, such as {% widget my_place long %}.