IF YOU'VE been developing htmx apps for a while, you might have tried to cache the HTML fragments generated by your server as htmx responses. Caching htmx fragments is the equivalent of caching JSON responses in a SPA. Eg, you might have a fragment response from GET /users/:id that renders a user detail view. You might want to cache this view to avoid expensive queries in the backend if you know the user details haven't changed.
But when you start caching htmx fragments, a problem pops up: the style doesn't match the rest of your app. You might be rapidly iterating on the app and making adjustments (small or big) to its CSS. You quickly start to notice that annoyingly frequently, your user fragments are not updating to the latest style.
Sure, you can do a hard reload and force the fragment to have the latest style. But surely there must be an easier way?
Content negotiation
Enter the version headers:
-
Accept-Version: a request header set by your frontend to instruct the backend what version of a resource it wants -
Version: a response header set by your backend to inform the frontend what version of the resource it is serving.
Basically, the backend and frontend have to agree on the version, otherwise they automatically do a hard reload.
You can think of this as a lightweight form of content negotiation. Here's a pseudo-code for a backend middleware that shows the rules:
if Accept-Version header not in request then
continue with request pipeline
else if Accept-Version header value = the expected version then
continue with request pipeline
else if request method is GET then
respond with 200 OK empty body and a response header HX-Redirect: request target
else
continue with request pipeline
finally
add response header Version: expected version
end
The meat of this middleware is the redirect if the expected and actual versions don't match. This ensures that the response htmx fragment style can't drift out of sync with the rest of the app.
Now, let's look at some of the details that make this middleware work.
The app version
The app must some version number readily available, and which accurately reflects the styles of the running version of the app. Many backend apps have some built-in version number in them; maybe the git commit sha, or maybe the modified timestamp of the app's executable. The point is it has to be something that reflects and changes when the app itself is changed.
Then, you have to inject this version number into every htmx request as a header, like this:
<script>
document.addEventListener('htmx:configRequest', evt => {
evt.detail.headers['Accept-Version'] = '$THE_VERSION';
});
</script>
You just have to ensure this script is injected into the bottom of every HTML page. This is easy with most HTML generation systems if you use their 'layouts' or similar feature.
This makes htmx add the Accept-Version header to every request that it makes. Notably, for normal browser requests, like navigations and redirects, this header is not added.
GET requests only
We are not touching POST or other requests–only GET request responses are cached anyway.
The full page redirect
Notice that when the versions don't match, we're getting htmx to do a redirect to the requested url. Eg, the request might have been for the fragment of GET /users/1, the style didn't match, so we redirected to /users/1. This means that we have to be able to serve both a fragment and a full page from every user-visible GET endpoint. Indeed, in my opinion this is a cornerstone of htmx usage. I've discussed it in more detail previously.
Finally
Notice that we have a 'finally' block at the end. This just means that even for responses where we 'continue with the request pipeline', we have to ensure we add the Version response header after processing the request and just before sending the response.
Astute readers might observe that the Version response header doesn't really do anything in this 'content negotiation' logic. That's true, it's really more of a debugging tool. If you ever find that the middleware is not working correctly, it will help you to very that the versions are in sync.
Top comments (0)