jmtd → log → Progressively enhancing CGI apps with htmx
I was interested in learning about htmx, so I used it to improve the experience of posting comments on my blog.
It seems much of modern web development is structured around having a JavaScript program on the front-end (browser) which exchanges data encoded in JSON asynchronously with the back-end servers. htmx uses a novel (or throwback) approach: it asynchronously fetches snippets of HTML from the back-end, and splices the results into the live page. For example, a htmx-powered button may request a URI on the server, receive HTML in response, and then the button itself would be replaced by the resulting HTML, within the page.
I experimented with incorporating it into an existing, old-school CGI web app: IkiWiki, which I became a co-maintainer of this year, and powers my blog. Throughout this project I referred to the excellent book Server-Driven Web Apps with htmx.
Comment posting workflow
I really value blog comments, but the UX for posting them on my blog was a bit clunky. It went like this:
you load a given page (such as this blog post), which is a static HTML document. There's a link to add a comment to the page.
The link loads a new page which is generated dynamically and served back to you via CGI. This contains a HTML form for you to write your comment.
The form submits to the server via HTTP POST. IkiWiki validates the form content. Various static pages (in particular the one you started on, in Step 1) are regenerated.
the server response to the request in (3) is a HTTP 302 redirect, instructing the browser to go back to the page in Step 1.
First step: fetching a comment form
First, I wanted the "add a comment" link to present the edit box in the current page. This step was easiest: add four attributes to the "comment on this page" anchor tag:
hx-get="<CGI ENDPOINT GOES HERE>"
- suppresses the normal behaviour of the tag, so clicking on it doesn't load a new page.
issues an asynchronous HTTP GET to the CGI end-point, which returns the full HTML document for the comment edit form
hx-select=".editcomment form"
- extract the edit-comment form from within that document
hx-swap=beforeend
andhx-target=".addcomment"
- append (courtesy of
beforeend
) the form into the source page after the "add comment" anchor tag (.addcomment
)
Now, clicking "comment on this page" loads in the edit-comment box below it without moving you away from the source page. All that without writing any new code!
Second step: handling previews
In the traditional workflow, clicking on "Preview" loaded a new page containing the edit form (but not the original page or any existing comments) with a rendering of the comment-in-progress below it. I wasn't originally interested in supporting the "Preview" feature, but I needed to for reasons I'll explain later.
Rather than load new pages, I wanted "Preview" to insert a rendering of the comment-in-progress being inserted into the current page's list of comments, marked up to indicate that it's a preview.
IkiWiki provides some templates which you can override to customise your site.
I've long overridden page.tmpl
, the template used for all pages. I needed to
add a new empty div
tag in order to have a "hook" to target with the
previewed comment.
The rest of this was achieved with htmx attributes on the "Preview"
button, similar to in the last step: hx-post
to define a target URI
when you click the button (and specify HTTP POST); hx-select
to filter
the resulting HTML and extract the comment; hx-target
to specify where
to insert it.
Now, clicking "Preview" does not leave the current page, but fetches a rendering of your comment-in-progress, and splices it into the comment list, appropriately marked up to be clear it's a preview.
Third step: handling submitted comments
IkiWiki is highly configurable, and many different things could happen once you post a comment.
On my personal blog, all comments are held for moderation before they are published. The page you were served after submitting a comment was rather bare-bones, a status message "Your comment will be posted after moderator review", without the original page content or comments.
I wanted your comment to appear in the page immediately, albeit marked up to indicate it was awaiting review. Since the traditional workflow didn't render or present your comment to you, I had to cheat.
handling moderated comments
One of my goals with this project was not to modify IkiWiki itself. I had to break this rule for moderated comments. When returning the "comment is moderated" page, IkiWiki uses HTTP status code 200, the same as for other scenarios. I wrote a tiny patch to return HTTP 202 (Accepted, but not processed) instead.
I now have to write some actual JavaScript. htmx emits the htmx:beforeSwap
event after an AJAX call returns, but before the corresponding swap is
performed. I wrote a function that is triggered on this event, filters for
HTTP 202 responses, triggers the "Preview" button, and then alters the
result to indicate a moderated, rather than previewed, comment. (That's why I
bothered to implement previews).
You can read the full function here: jon.js.
Summary
I've done barely any front-end web development for years and I found working with htmx to be an enjoyable experience.
You can leave a comment on this very blog post if you want to see it in action. I couldn't resist adding an easter egg: Brownie points if you can figure out what it is.
Adding htmx to an existing CGI-based website let me improve one of the workflows in a gracefully-degrading way (without JavaScript, the old method will continue to work fine) without modifying the existing application itself (well, almost) and without having to write very much code of my own at all: nearly all of the configuration was declarative.
Comments
Hah, interesting. You see the latency on "comment on this page", and there is some very brief flicker as the DOM is changed, but this is indeed cool.
I hope your changes land in ikiwiki soon (well, when I upgrade to Trixie), because I still have some use for it. Thanks!
I was here 😄
But seriously, this is a good blog post. I love seeing tools in action, and I might finally try HTMX. I was hesitant, to be honest, because it seemed "weird" to me at first.