prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

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:
Minternal/web/scripts/modules/header.js | 49+++++++++++++++++++++++++++++++++++++++++++++++++
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); }