Photo by Jens Johnsson: https://www.pexels.com/photo/brown-wooden-arrow-signed-66100/

Effortless Page Routing Using HTMX

Paul Allies
5 min readDec 6, 2023

React can often be excessive for your web application, and there are instances when utilising only a web server alongside HTMX can produce equivalent results for creating an interactive application.

In this blog post, we will illustrate how HTMX can be used to build interactive, flicker-free page navigation:

Server Setup

mkdir no-react-app 
cd no-react-app
npm init -y
npm install express nunjucks

then we create a server file and run

//File: app.js
const express = require("express")
const app = express()

const nunjucks = require('nunjucks');
nunjucks.configure("views", {
autoescape: true,
express: app
});

app.get("/", (req, res) => {
res.render("pages/home.html")
})

app.get("/users", (req, res) => {
res.render("pages/users.html")
})

app.get("/posts", (req, res) => {
res.render("pages/posts.html")
})

app.listen(3000, () => {
console.info(`Application running http://localhost:3000`)
})

We are using nunjucks as a templating engine. All templates, layouts, and partials are to be stored in the “views” directory. Our project structure will therefore be as follows

App Structure

app.js
views
layouts
main.html
partials
sidenav.html
pages
user.html
home.html
posts.html

Because we’re using a template engine, let’s add a layout in which all views will extend

Main Layout

<!--File: views/layouts/main.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.0.2/tailwind.min.css" />
<title>HTMX App</title>
</head>

<body class="bg-gray-200">
<div class="flex h-screen">
<!-- Side Navigation -->
{%- include('partials/sidenav.html')%}

<!-- Main Content Area -->
<div class="w-full bg-white p-4" id="main">
{% block content %}{% endblock %}
</div>
</body>

</html>

We have refactored a sidenav template component into partials and included this in our layout

Side Nav Component

<!--File: views/partials/sidenav.html-->
<div class="w-56 bg-gray-800 text-white p-4">
<a href="/" class="block py-2 px-4 text-white hover:bg-gray-600">Home</a>
<a href="/users" class="block py-2 px-4 text-white hover:bg-gray-600">Users</a>
<a href="/posts" class="block py-2 px-4 text-white hover:bg-gray-600">Posts</a>
</div>

and created our main pages, home.html, users.html and posts.html

Pages

<!--views/pages/home.html-->
{% extends 'layouts/main.html' %}

{% block content %}
<h1 class="text-2xl font-bold mb-4">HTMX Nav</h1>
{% endblock %}
<!--views/pages/users.html-->
{% extends 'layouts/main.html' %}

{% block content %}
<h1 class="text-2xl font-bold mb-4">Users</h1>
{% endblock %}
<!--views/pages/posts.html-->
{% extends 'layouts/main.html' %}

{% block content %}
<h1 class="text-2xl font-bold mb-4">Posts</h1>
{% endblock %}

When we run the server we have navigation, but with full page reloads:

We must address this issue by incorporating a lightweight JavaScript library known as HTMX. This library can greatly enhance the user navigation experience by making it more seamless and interactive. It’s important to note that HTMX has a broader range of applications, but for our current purposes, we will focus solely on leveraging its capabilities to achieve smoother navigation.

Progressive Enhance with HTMX

The fastest way to get going with HTMX is to load it via a CDN. You can simply add this to your head tag and get going:

<!--File: views/layouts/main.html-->
...
<script src="https://unpkg.com/htmx.org@latest"></script>
<title>HTMX App</title>
</head>
...

We now can make a small change to the sidenav

  1. Remove href attributes and replace with hx-get attributes. When a user clicks on this link, an HTTP GET request is issued.
  2. Add hx-target attribute either to each anchor or one to the anchor parent div. The hx-target attribute allows you to target an element for swapping the response of the hx-get.
  3. Add hx-push-url=”true” to each anchor. The hx-push-url attribute allows you to push a URL into the browser location history. This creates a new history entry, allowing navigation with the browser’s back and forward buttons.

What does this do: We are declaratively instructing the HTMX lib to make a server call when the anchor is clicked and insert the response into the div of id ‘main’

<div class="w-56 bg-gray-800 text-white p-4" hx-target="#main"  >
<a hx-get="/" hx-push-url="true" class="block py-2 px-4 text-white hover:bg-gray-600">Home</a>
<a hx-get="/users" hx-push-url="true" class="block py-2 px-4 text-white hover:bg-gray-600">Users</a>
<a hx-get="/posts" hx-push-url="true" class="block py-2 px-4 text-white hover:bg-gray-600">Posts</a>
</div>

We now have the following.

We solved the flicker issue and the url correctly pushes to the new url if we might want to share the navigation

Make App HTMX Aware

We need to determine whether each incoming server request is an HTMX call or not. If it is, we must instruct the template engine to skip using the layout and simply return the HTML of that template. To achieve this, we’ll need to incorporate specific middleware:

//File: app.js
...
app.use((req, res, next) => {
res.locals.useLayout = req.headers["hx-request"] !== "true";
next();
})

app.listen(3000, () => {
console.info(`Application running http://localhost:3000`)
})

Use the layout only if an HTMX request is not detected.

<!--File: views/layouts/main.html-->
{% if useLayout %}
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.0.2/tailwind.min.css" />
<script src="https://unpkg.com/htmx.org@latest"></script>
<title>HTMX App</title>
</head>
<body class="bg-gray-200">
<div class="flex h-screen">
<!-- Side Navigation -->
{%- include('partials/sidenav.html')%}
<!-- Main Content Area -->
<div class="w-full bg-white p-4" id="main">
{% endif %}

{% block content %}{% endblock %}

{% if useLayout %}
</div>
</body>
</html>
{% endif %}

We’ve successfully accomplished a seamless and flicker-free navigation experience that allows for sharing URLs.

Original Blog Post: https://nanosoft.co.za/blog/post/express-htmx

Code: https://github.com/nanosoftonline/express-htmx

--

--