Using htmx without hurting your SEO
htmx has a reputation for being safe for search engines, and it mostly deserves it - but the safety comes from a particular way of using it, not from the library. It is entirely possible to build something uncrawlable with htmx.
What a crawler is actually doing
A crawler requests a URL and reads the HTML that comes back. Modern search crawlers will also render the page with a browser engine and execute JavaScript, but that rendering happens on a separate, slower, lower-priority pass. Anything that only exists after rendering is discovered late, if at all, and anything that requires a user interaction to appear is not discovered at all - a crawler does not click your tabs.
So the question for any htmx page is simple: what is in the HTML that came back from the first request, before any swap has happened?
Rule one: every URL is a complete document
This is the one that matters most. hx-boost intercepts link clicks
and fetches the target page with AJAX, swapping the new body into place. It
feels like a single-page application, but the crucial detail is that the URL it
fetched is a real page. A crawler that requests it directly gets a complete
document with its own <title>, its own meta description and
its own canonical link.
That is why boosting is safe and a client-side router usually is not. The enhancement is invisible to anything that does not run it.
Rule two: fragments must not be indexable pages
The partials this site serves from /partials are fragments -
<div> soup with no <head>, no title and
no navigation. If a crawler finds one, it will happily index a thin,
contextless page that competes with the real one.
Two things prevent that. The fragments are never linked with an
<a href> - they are only referenced from
hx-get attributes, which a crawler does not follow. And the host
sends a header on that path:
X-Robots-Tag: noindex
Belt and braces, because the cost of getting this wrong is a set of duplicate thin pages in the index and the cost of the header is one line of configuration.
Rule three: the default state ships in the HTML
The tabbed section on the home page loads its panels over the network. But the
first panel is not empty waiting for a fetch - the same markup that
/partials/principles returns is also written directly into the
page. The htmx request only happens when someone selects a different tab.
This costs a small amount of duplication and buys two things: the content is
visible to a crawler on the first request, and the page is useful before any
JavaScript has run. If you find yourself with a page whose main content area is
empty until hx-trigger="load" fires, you have built a
client-rendered page with extra steps.
The title problem
One genuine gap: when hx-boost swaps a page in, htmx updates the
document title but not the rest of the head. The canonical link, the meta
description and the Open Graph tags stay as they were on the previous page.
This does not affect crawlers, which request each URL fresh. It does affect
anything reading the live DOM - some analytics tools, and a person checking
their work in devtools. The head-support extension merges the
incoming head properly and is worth the two kilobytes:
<body hx-boost="true" hx-ext="head-support">
Checking your work
Disable JavaScript and browse the site. Every link should still navigate, every form should still submit, and every page should still show its content. If that holds, the htmx layer is doing what it should: making a working site feel faster, rather than being the thing that makes it work.