Layouts
Layouts are templates that can be extended by other layouts or pages to provide common structure or content across pages. This enables you to create a layout, for example, that includes a header and footer so that individual pages can extend that layout and provide only the blocks that are necessary as oppose to each page having to individually include the header and footer as a partial.
Layouts are tied to Twig’s extends
tag. Velocity provides a “main” layout which provides the overall HTML structure for velocity enabled pages. This layout contains only two blocks which are otherwise placed in the context of a simple best-practices HTML5 skeleton, namely, the body
and head
block.
Your layouts should, generally speaking, extend this layout and provide the body
. For example resources/layouts/main.html
:
{% extends '@layouts/velocity/main.html' | proxy('body') %}
{% block body %}
<x:: hx-target:="#main">
<header>
<!-- Common Header/Navigation/Etc -->
</header>
<main id="main">
{% block main %}
{% endblock %}
</main>
<footer>
<!-- Common Footer -->
</footer>
</x::>
{% endblock %}
Note the proxy()
filter being used, more on this later.
You are free to overload the head, however, keep in mind that this is where requisite scripts and styles for default Velocity functionality come from. You can have a look at vendor/hiraeth/velocity/resources/layouts/head.html
and incorporate or modify various pieces of it in an overloaded head
block. You may, for example, wish to use a different CDN, or use local copies of various libraries, or include built Tailwind output instead of using the CDN. For most use cases where you’re simply trying to add new scripts or styles you can just define a custom head
block and include Velocity’s:
{% block head %}
{% include '@layouts/velocity/head.html' %}
<script>
// My Custom JS
</script>
{% endblock %}
Targeting
When you create a layout, in most cases you will want to re-target boosted HTMX requests. The hx-target
attribute in our example above is set on the #main
id. Since we want all boosted links (including anything in our header navigation and footer) to load in our <main>
tag we need to set the attribute on all three of these sections. To do this more easily, we made use of the “fragment” tag (<x::>
, more on this elsewhere) but for now, just know that the attribute will be added to all three <header>
, <main>
and <footer>
.
The default target in Velocity’s main layout (which we extended) is the <body>
tag.
Proxying
Proxying is a critical and important concept to understand how Velocity works. It is closely linked to targeting in that the ID of an hx-target
determines the “level” of rendering being performed. Because the goal of HTMX is, in part, not to need to re-render entire pages for what amounts to only partial DOM changes, server-side frameworks need a way to determine which content needs to be rendered and returned.
In Velocity, this is done by pairing Twig blocks to the hx-target
’s ID. If a request to a page targets #body
the page should be rendered all the way up to the body
block, and no further.
Direct Requests
Direct requests to a page do not use proxying. A page that extends a layout will be rendered in full when it is directly requested, all the way to its highest level layout as represented by the onion below:
@layouts/velocity/main.html
@layouts/main.html
@pages/@index.html
[ block main ]
Boosted Requests
A boosted request enables proxying to be performed. Although we’ve already seen an example in our resources/layouts/main.html
example above, let’s cover how to make use of proxying in the context of layouts and pages. To do this, we’ll begin defining our first page at resources/pages/@index.html
.
Filtering
The proxy filter is placed within the context of a Twig extends
tag:
{% extends '@layouts/main.html' | proxy('main') %}
This basically tells Velocity which blocks (and correspondingly which hx-target
IDs) the current template supports. If the current hx-target
ID is not in the list of blocks, the template will be extended. Accordingly, the layout or extending template can then define which blocks it proxies using the same filter. Rendering ends when the first template to provide the block for the requested hx-target
ID is found. Now that we’ve defined what we can proxy, let’s go ahead and define the block.
{% block main %}
<p>
Enim fugiat minima perspiciatis harum autem itaque.
Est nesciunt aut consequatur ipsum. Adipisci iste
repellendus quia adipisci. Voluptate temporibus
dolores laborum voluptatum.
</p>
{% endblock %}
Given this:
- A boosted request to
/
with anhx-target
of#main
will render only the<p>
(themain
block) - A boosted request to
/
with anhx-target
of#body
will render the surrounding@layouts/main.html
content which proxiesbody
which includes the header, footer, main element itself.
Providing Multiple Blocks
A single layout or page can provide multiple blocks. This is particularly useful when rendering content that needs to be swapped “out of bounds.” An out of bounds (OOB) swap is when an element returned in an HTMX requests should be swapped somewhere outside of the target. You can add multiple arguments to the proxy()
filter to indicate the template can provide multiple blocks and therefore content for multiple targets. Take into account:
- All blocks at a given level are rendered, even if not returned.
- The first block proxied SHOULD BE the primary block / target.
- When the
hx-target
matches the first proxied block, all blocks are returned, enabling OOB swaps.
- When the
- Secondary blocks CAN BE independently requested.
- When the
hx-target
matches only a secondary block, only that block is returned. To elucidate this further, let’s make some adjustments to our previous work.
- When the
Adding a Centralized Header
One common use case is to have a centralized <header>
or <h1>
. Without re-rendering a full page, we need a way to get the header or heading content as provided by an individual page into this centralized “shared” location. Let’s first update our main layout to include a new block for this:
{% extends '@layouts/velocity/main.html' | proxy('body') %}
{% block body %}
<x:: hx-target:="#main">
<header>
<!-- Common Navigation -->
{% block hero %}
{% endblock %}
</header>
<main id="main">
{% block main %}
{% endblock %}
</main>
<footer>
<!-- Common Footer -->
</footer>
</x::>
{% endblock %}
Following this, we’ll update our page to proxy and define its “hero” content:
{% extends '@layouts/main.html' | proxy('main', 'hero') %}
{% block hero %}
<section
id="hero"
{% if not proxy('hero') %}
hx-swap-oob="true"
{% endif %}
>
<h1>
Hello Velocity!
</h1>
</section>
{% endblock %}
{% block main %}
<p>
Enim fugiat minima perspiciatis harum autem itaque.
Est nesciunt aut consequatur ipsum. Adipisci iste
repellendus quia adipisci. Voluptate temporibus
dolores laborum voluptatum.
</p>
{% endblock %}
Using the proxy()
function (not to be confused with the filter), we ensure effectively that if we’re not proxying the hero
block directly, then we want to perform an out of bounds swap. As you may have guessed, and as was eluded to before, this means that a request could be made to /
with an hx-target
ID of hero
, in which case, only the hero
block would be returned and the hx-swap-oob
would not be present.