The Problem
Page navigation in static websites can be better. Typically, when a user clicks a link, the next page is fetched and the entire content is parsed and loaded, replacing the previous page entirely. Most of the content within the <head></head>
tag of a web page doesn’t change between pages of a website, but the browser will check anyway, and maybe we don’t want that. Also, if the next page loads too fast, we may want to show a nice animation during a page transition.
The Solution
One way of getting around this page navigation issue has been to write a single page application (SPA) instead of a static website. Since I just want to enhance an existing static website (this blog) I will not be changing it to a single page application.
Another popular solution is to change the behavior of links to asynchronously fetch the next page, updating the document <body>
content without a full page load. This makes page navigation much faster. I’m going to use htmx to achieve that behavior for this blog. This solution can be done in just a few lines of code. Some helpful blog posts have been written about it, but this post is about how I did it for this blog.
I chose htmx as a solution because it is a small JavaScript library that enhances any website with SPA-like behavior. The boosting feature of htmx globally enables asynchronous requesting of body content. A great feature of the boost feature is that it “degrades gracefully”, like how an escalator becomes stairs, if something goes wrong in the user’s browser. I find htmx attributes easy to use and its conventions easy to follow.
Customize Hugo Theme
I’m using the PaperMod theme for my developer blog, so the steps to customize it are somewhat specific to this theme.
Add htmx
The first step is to add htmx to the website’s <head>
section. I ran this command:
curl https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js > assets/vendor/htmx.min.js
This partial layout/partials/extend_head.html
is automatically included by the theme in the <head>
during the build process.
// layouts/partials/extend_head.html
{{ with resources.Get "vendor/htmx.min.js" }}
<script src="{{ .RelPermalink }}"></script>
{{ end }}
Edit the base template
Hugo uses a lookup order to determine which layout files to render. In my project I copied the layout from the theme to my layouts folder, preserving the path, so Hugo will use it instead of rendering the theme’s original layout file.
I added global boosting to baseof.html
from the PaperMod theme. It looks like this once I add hx-boost
to it:
|
|
Add the page transition with CSS
The boosted links will now “swap” asynchronously fetched content into the body. During the swap process, htmx adds and removes CSS classes that we can use to add some animations.
I only want the main
element to receive the transition here, so my and do not flash in and out needlessly. The aim is to keep the transition simple and tasteful here.
main {
transition: opacity 200ms ease-in;
}
/* htmx automatically adds .htmx-added to new DOM elements when they are swapped in */
main.htmx-added {
opacity: 0;
}
When the main tag is swapped in by htmx, it enters the DOM with .htmx-added
class that is quickly removed.
Hold Up
That should be it, right? Well, there is a problem with my theme. It adds a script for the “scroll to top” functionality. I see a strange error in Chrome:
Uncaught SyntaxError: Failed to execute 'insertBefore' on 'Node': Identifier 'menu' has already been declared
Firefox is a little more clear:
Uncaught SyntaxError: redeclaration of let menu
The error occurs in a script my theme PaperMod adds to the page in the body tag:
|
|
Mysteriously, another script further down the theme’s code doesn’t throw an error:
|
|
What’s going on?
Our script crashes when using let
to redeclare a window
scoped variable. Remember that any variable declared in a <script></script>
tag is automatically scoped to the window
. A little gotcha when using this page navigation technique is that the window
still has the same variables we declared in the last page!
In a normal page transition, this is not a problem, but in our case, the menu
variable is already in the window scope and is being redeclared with let
on every page navigation.
Moreover, var
simply allows you to redeclare it in the same scope, but let
is stricter and will throw an error if you try to redeclare it in the same scope! To fix this I simply copy the footer.html
from my theme and fix the offending script.
|
|
By changing the let
to var
we keep the code simple and avoid errors. Sometimes the simplest fix is the best one.
Conclusion
That’s it! My blog gets a little UX boost with the slick page transition, and feels a little bit faster. An important difference between let
and var
was revealed in the process.