commit d39d656a4bccbfb736a93cc8b420a92caa6e79d9
parent 4e02a39219091a5c4f2889fb0b3a823525e85ab8
Author: Damian Myrda <damian@prime8.dev>
Date: Thu, 22 Oct 2026 01:27:55 +0000
web: links into the app (relative or prime8.dev) swap content instead of reloading
Diffstat:
1 file changed, 49 insertions(+), 0 deletions(-)
diff --git a/internal/web/scripts/modules/header.js b/internal/web/scripts/modules/header.js
@@ -4,8 +4,33 @@ const headerHandlers = {
keydown: null,
afterSwap: null,
popstate: null,
+ linkClick: null,
};
+// Hosts that serve this same app, so a link to any of them stays in-page.
+const APP_HOSTS = new Set(["prime8.dev", "www.prime8.dev"]);
+// Pages the app renders into #content; anything else (raw files, the API,
+// static assets) is a real load.
+const APP_PATH = /^\/(?:(?:projects|experience)(?:\/[^/]+)?)?\/?$/;
+
+// appPath returns the in-app path an anchor points at, or "" when clicking
+// it should be left to the browser.
+function appPath(a) {
+ if (a.hasAttribute("download") || a.hasAttribute("hx-get")) return "";
+ let url;
+ try {
+ url = new URL(a.href, window.location.href);
+ } catch {
+ return "";
+ }
+ if (!/^https?:$/.test(url.protocol)) return "";
+ const sameApp =
+ url.origin === window.location.origin ||
+ (APP_HOSTS.has(url.hostname) && APP_HOSTS.has(window.location.hostname));
+ if (!sameApp || !APP_PATH.test(url.pathname)) return "";
+ return url.pathname.replace(/\/+$/, "") || "/";
+}
+
export function initHeader() {
const headerEl = document.getElementById("tabs");
if (!headerEl) return;
@@ -34,6 +59,30 @@ export function initHeader() {
link.click();
}
+ // Every link into the app (relative, or absolute to prime8.dev, even with
+ // target=_blank) swaps #content instead of reloading the page. Modified
+ // clicks (new tab, new window) are left alone.
+ function navigate(path) {
+ const tab = path === "/" ? fallback : path.slice(1).split("/")[0];
+ setActiveTabUI(tab);
+ if (window.location.pathname !== path) history.pushState({ tab }, "", path);
+ window.htmx.ajax("GET", path, { target: "#content", swap: "innerHTML" });
+ }
+
+ if (headerHandlers.linkClick) {
+ document.removeEventListener("click", headerHandlers.linkClick);
+ }
+ headerHandlers.linkClick = (e) => {
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
+ const a = e.target.closest?.("a[href]");
+ if (!a || !window.htmx) return;
+ const path = appPath(a);
+ if (!path) return;
+ e.preventDefault();
+ navigate(path);
+ };
+ document.addEventListener("click", headerHandlers.linkClick);
+
if (headerHandlers.keydown) {
document.removeEventListener("keydown", headerHandlers.keydown);
}