Pages

Pages are templates that represent a complete externally accesisble request independent of whether or not that request was initiated via HTMX or directly from the user agent (client). A page can be added in velocity simply by adding a file under resources/pages which corresponds to the URL/path which should resolve that page.

The most basic example is resources/pages/@index.html which would correspond to the / request path. Similarly resources/pages/@about-us.html would correspond to /about-us.

Page layout

While it’s possible to write a page without any additional layout, most pages will extend a layout and provide one or more blocks to fullfill its dependencies. The most basic example of a page that uses velocity would be something like this:

{% extends '@layouts/velocity/main.html' | proxy('body') %}

{% block body %}
    ...
{% endblock }

In the example above, we include the main Velocity layout and simply provide the body block. In most cases, you’ll actually want to create your own layout to extend the main layout and include common elements. Then your pages would extend that. This would prevent, for example, constant reloading of header/footer and/or common navigation elements.

Canonoical URLs

There is a distinction between resources/pages/@settings.html and resources/pages/settings/@index.html. The former indicates a canonical URL of /settings while the latter indicates a canonical url of /settings/. Velocity will automatically redirect when a user hits a non-canonical URL for which there is a canonical alternative (i.e. adding the / or removing it depending on the file found). If both files exist, these could, in principle, indicate two distinct resources.

Matchers

While direct 1-to-1 mappings are a really great solution for one off pages, they’re a poor subtitute for many modern web reqeusts which require dynamic URL segments that map to specific parameters. Ergo, it’s possible to have a URL such as /users/1 where 1 indicates a user id. To do this, you create a ~matchers.jin file in the directory that corresponds to the appropriate URL segment. In this example, we’d create a resources/pages/users/~matchers.jin file with contents like the following:

[detail]

    pattern = ([1-9][0-9]*)
    mapping = [
        "id"
    ]

With this in place our request to /users/1 will now match the pattern. The first defined capture group will map to a parameter called id, and the the template that will be resolved is resources/pages/users/@detail.html. Within the template you can access the parameter as follows:

<p>The user ID is {{ parameters.id }}</p>

Control Logic

At this point, you may be wondering… if requests are routed directly to templates, how do we perform required control logic? Where does the more complex stuff go? Velocity takes a “view-first” approach, which means unlike traditional web-MVC approaches your template calls your control logic rather than your controller calling your template. Control logic can “interrupt” the request, but we believe this view-first approach provides a much more accurate model as to how web sites and applications are developed.

To invoke control logic, you simply use the do tag built into twig, combined with the action() function provided by Velocity:

{% do action('My:App:GetUser') %}

In the example above, this call would call the My\App\GetUser action passing any parameters to the __invoke() method by name. A hypothetical example of what that might look like:

<?php

namespace My\App;

use Hiraeth\Actions\AbstractAction;

class GetUser extends AbstractAction
{
    public function __construct(
        protected UserRepository $users
    ) {}

    public function __invoke(int $id)
    {
        $user = $this->users->find($id);

        if (!$user) {
            return $this->response(404);
        }

        return [
            'user' => $user
        ]
    }
}

Critical points to note:

  • The $id parameter is automatically resolved from our parameters.id (via our ~matchers.jin) file.
  • We can return and interrupted response, in this case a 404 if the user is not found. This will interrupt template rendering and return the response instead.
  • The __construct() dependencies are autowired and injected. For more about this, see Hiraeth’s dependency injector docs.

Once we’ve called the control logic, the returned array becomes part of our template context. So, immediately after our do tag, we can us the user in our page template:

<p>
    First Name: {{ user.firstName }}
    Last Name: {{ user.lastName }}
</p>