Note: I'm not sure if this is necessarily fixable with htmx due to the way the browsers behave, but it might be something that needs to be highlighted in the documentation.
Use case: I'm using hx-boost or hx-get+hx-push-url to return an HTML fragment rather than a whole page. The stripped down whole page looks like this:
<html>
<head>
...styling etc tags...
</head>
<body hx-boost="true" hx-select="#content" hx-swap="outerHTML">
..header content here
<div id="content"></div>
....footer content here
</body>
</html>
A typical URL returns an HTML fragment. It uses Django templates, but the result is that if the request has the HX-Header, the result looks something like this:
<div id="content">
my page
</div>
and if the request does not have the HX-header it returns the above content inside the full page - for example if reloading the page manually or opening the URL in the browser instead of navigating to the page inside the site. That ensures that we have very efficient page swaps, only returning the content we need.
Unfortunately there appears to be a problem when the browser preferences are set to restore the last tabs from cache when a browser is closed and re-opened. What appears to happen is that the browser just pulls the last GET response for that specific URL in the tab, whether it is a xhr response or not. The result is that if the last request was an htmx request returning a
fragment, then that fragment is restored instead of the full page, resulting in a broken page (as all surrounding CSS, JS, header/footer etc will be missing).
Setting htmx settings such as historyCacheSize or refreshOnHistoryMiss make no difference. Setting cache-control to no-cache in either the meta tags or HTTP responses make no difference. The browser appears to ignore all cache settings.
The only two workarounds I can find are:
1) Avoid returning html fragments in hx-boost and hx-push-url calls and return the entire body, using hx-select if you need to pull out a specific fragment from the response. This is workable but obviously far less efficient than being able to return html fragments, as you have to build parts of the page in the server you never need.
2) Add the following script to the start of your html fragments:
<script>
if (!window.doNotReload) {
location.reload();
}
</script>
and somewhere in your initial HTML page:
<script>
window.doNotReload = true;
</script>
This ensures that if the fragment is loaded from cache it will force a reload. It's a bit janky and may have other issues, but at least the user is not forced to manually reload.
This behaviour has been observed in Firefox and Chrome and in multiple devices. To replicate ensure you have browser settings to something like "reload last session" instead of some home page or new tab, navigate to a page with hx-boost or hx-push-url, close the browser and re-open.
Again, this might not be something necessarily fixable with htmx.That said, I did try this approach when returning the HTML fragment:
Update: it looks like the best workaround is to send Cache-Control: no-store, max-age=0 in the header. For example, I have a Django middleware like this:
class CacheControlMiddleware:
def __call__(self, request):
response = self.get_response(request)
if "HX-Request" in request.headers:
response["Cache-Control"] = "no-store, max-age=0"
return response
This appears to fix the issue with browser restarts at least on Firefox and Chromium browsers.
I am experiencing the same behaviour as described by @danjac with Google Chrome Version 90.0.4430.212
But i also noticed similar behaviour in another scenario: When navigating between a list and detailpage.
I have a newslisting page with some pagination below the listed items. The pagination links are wrapped like this:
<div class="c-pagination__wrapper" hx-target="closest .js-partialcontainer" hx-swap="innerHTML" hx-boost="true" hx-push-url="true">[implementation omitted]</div>
reproduction steps:
When i click a pagination hyperlink, a xhr request gets fired, and the listing on the page gets updated. All seems fine.
Then i click a newsitem hyperlink and get redirected to a page containing the item details. (this is a normal redirect, no htmx involved).
Now when i click the back button of the browser, it only displays the html fragment that was sent by the server in step 1.
In step 3 The browser Network tab shows 1 request:
Request URL: https://domainname.eu/news?page=2
Request Method: GET
Status Code: 200 (from disk cache)
Referrer Policy: strict-origin-when-cross-origin
Update on the issue reported by perplexErik (which is my other GH account).
After applying the "Cache-Control: no-store" header the browser started behaving better.
But wat weird is that de browser back button works correct when i click it once. But when i click the back button for a second time it does something weird. The URL gets updated, but the content stays the same.
Hey guys! Looking through the docs I found this, under the History Supportsection:
When a user hits the back button, htmx will retrieve the old content from storage and swap it back into the target, simulating "going back" to the previous state. If the location is not found in the cache, htmx will make an ajax request to the given URL, with the header HX-History-Restore-Request set to true, and expects back the HTML needed for the entire page. Alternatively, if the htmx.config.refreshOnHistoryMiss config variable is set to true, it will issue a hard browser refresh.
The django-htmx extension recognizes the HX-History-Restore-Resquest using request.htmx.history_restore_request. Maybe using this logic when generating the views will help:
def myview(request):
template_name = 'myapp/mytemplate.html'
if request.htmx and not request.htmx.history_restore_request:
template_name = 'myapp/mytemplate_partial.html'
...
Update: it looks like the best workaround is to send Cache-Control: no-store, max-age=0 in the header. For example, I have a Django middleware like this:
class CacheControlMiddleware:
def __call__(self, request):
response = self.get_response(request)
if "HX-Request" in request.headers:
response["Cache-Control"] = "no-store, max-age=0"
return response
This appears to fix the issue with browser restarts at least on Firefox and Chromium browsers.
A variation of this solution worked for me. I use Wagtail CMS and HTMX on the Homepage to deal with a lot of different filters. Once you go off that page from the results delivered via HTMX and press the back button in a Chromium browser, only the HTMX part of the page is delivered, leaving the site header and footer and body content blank.
This solution was not added to a middleware, but I suppose it still can be. I just needed HTMX to act nicely on my home page, so this went into the home page model
Here is my solution:
if "HX-Request" in request.headers:
headers = {"Cache-Control": "no-store, max-age=0"}
else:
headers = {}
return TemplateResponse(request, html_template, context, headers=headers)
Update: it looks like the best workaround is to send Cache-Control: no-store, max-age=0 in the header. For example, I have a Django middleware like this:
class CacheControlMiddleware:
def __call__(self, request):
response = self.get_response(request)
if "HX-Request" in request.headers:
response["Cache-Control"] = "no-store, max-age=0"
return response
This appears to fix the issue with browser restarts at least on Firefox and Chromium browsers.
This actually was the most straightforward solution when using django and django-htmx. Very much appreciated!
According to documentation: "refreshOnHistoryMiss defaults to false, if set to true htmx will issue a full page refresh on history misses rather than use an AJAX request"
Note: I'm not sure if this is necessarily fixable with htmx due to the way the browsers behave, but it might be something that needs to be highlighted in the documentation.
Use case: I'm using hx-boost or hx-get+hx-push-url to return an HTML fragment rather than a whole page. The stripped down whole page looks like this:
A typical URL returns an HTML fragment. It uses Django templates, but the result is that if the request has the HX-Header, the result looks something like this:
and if the request does not have the HX-header it returns the above content inside the full page - for example if reloading the page manually or opening the URL in the browser instead of navigating to the page inside the site. That ensures that we have very efficient page swaps, only returning the content we need.
Unfortunately there appears to be a problem when the browser preferences are set to restore the last tabs from cache when a browser is closed and re-opened. What appears to happen is that the browser just pulls the last GET response for that specific URL in the tab, whether it is a xhr response or not. The result is that if the last request was an htmx request returning a
Setting htmx settings such as historyCacheSize or refreshOnHistoryMiss make no difference. Setting cache-control to no-cache in either the meta tags or HTTP responses make no difference. The browser appears to ignore all cache settings.
The only two workarounds I can find are:
1) Avoid returning html fragments in hx-boost and hx-push-url calls and return the entire body, using hx-select if you need to pull out a specific fragment from the response. This is workable but obviously far less efficient than being able to return html fragments, as you have to build parts of the page in the server you never need.
2) Add the following script to the start of your html fragments:
and somewhere in your initial HTML page:
This ensures that if the fragment is loaded from cache it will force a reload. It's a bit janky and may have other issues, but at least the user is not forced to manually reload.
This behaviour has been observed in Firefox and Chrome and in multiple devices. To replicate ensure you have browser settings to something like "reload last session" instead of some home page or new tab, navigate to a page with hx-boost or hx-push-url, close the browser and re-open.
Again, this might not be something necessarily fixable with htmx.That said, I did try this approach when returning the HTML fragment:
Thus returning a truncated page but with required meta tags.
So in theory,
refreshOnHistoryMiss
should force a reload, but this does not happen.Update: it looks like the best workaround is to send Cache-Control: no-store, max-age=0 in the header. For example, I have a Django middleware like this:
This appears to fix the issue with browser restarts at least on Firefox and Chromium browsers.
I am experiencing the same behaviour as described by @danjac with Google Chrome Version 90.0.4430.212 But i also noticed similar behaviour in another scenario: When navigating between a list and detailpage.
I have a newslisting page with some pagination below the listed items. The pagination links are wrapped like this:
<div class="c-pagination__wrapper" hx-target="closest .js-partialcontainer" hx-swap="innerHTML" hx-boost="true" hx-push-url="true">[implementation omitted]</div>
reproduction steps:
In step 3 The browser Network tab shows 1 request: Request URL: https://domainname.eu/news?page=2 Request Method: GET Status Code: 200 (from disk cache) Referrer Policy: strict-origin-when-cross-origin
Hope this helps.
Update on the issue reported by perplexErik (which is my other GH account). After applying the "Cache-Control: no-store" header the browser started behaving better.
But wat weird is that de browser back button works correct when i click it once. But when i click the back button for a second time it does something weird. The URL gets updated, but the content stays the same.
Repro path using pagination example
<div class="c-pagination__wrapper" hx-target="closest .js-partialcontainer" hx-swap="innerHTML" hx-boost="true" hx-push-url="true"><a href="?page=1">1</a><a href="?page=2">2</a><a href="?page=3">3</a></div>
Expected behaviour in step 7:
Hey guys! Looking through the docs I found this, under the
History Support
section:The
django-htmx
extension recognizes theHX-History-Restore-Resquest
usingrequest.htmx.history_restore_request
. Maybe using this logic when generating the views will help:My solution was to just set
This also fixed an issue with client DOM changes (e.g. modals opened using AlpineJS) from being triggered on cache reload.
A variation of this solution worked for me. I use Wagtail CMS and HTMX on the Homepage to deal with a lot of different filters. Once you go off that page from the results delivered via HTMX and press the back button in a Chromium browser, only the HTMX part of the page is delivered, leaving the site header and footer and body content blank.
This solution was not added to a middleware, but I suppose it still can be. I just needed HTMX to act nicely on my home page, so this went into the home page model
Here is my solution:
This actually was the most straightforward solution when using django and django-htmx. Very much appreciated!
Would setting the Vary header not be a suitable fix for this?
The problem was solved for me by following settings:
According to documentation: "refreshOnHistoryMiss defaults to false, if set to true htmx will issue a full page refresh on history misses rather than use an AJAX request"
Reflecting the discussion on https://www.reddit.com/r/htmx/comments/1aw5iqa/page_fragment_after_page_tombstones/ the proper backend solution seem to be setting response header:
Vary: HX-Request
(see https://htmx.org/docs/#caching).