remi.works
Technical overview
This was the Eleventy version of remi.works, before the SvelteKit rebuild.
I wanted a personal site I could publish as static files, with enough room to play with colors, navigation, and canvas effects.
GitHub Pages handled hosting, and the browser code was plain HTML, CSS, and JavaScript.
Architecture
I kept Eleventy's templates, data files, and authored HTML fragments, then split the browser behavior into ES modules.
app.js connected search, the router, and page setup. Imports made the dependencies explicit, and esbuild split route
and demo modules into chunks that could load when they were needed.
import { initSearch } from "./components/search-modal.js";
import { highlightWithin } from "./components/syntax-highlight.js";
import { createPageRouter } from "./core/router.js";
import { normalizePath, resolveRoute } from "./pages/routes.js";
const search = initSearch();
const router = createPageRouter({
resolveRoute,
normalizePath,
beforeSwap() {
search.close({ restoreFocus: false });
},
contentReady({ main }) {
return highlightWithin(main);
}
});
router.start();
Content pipeline
Eleventy read the metadata indexes and included the article fragments in the generated detail pages. Lists, the homepage, and search loaded the JSON indexes in the browser. A shared loader cached the results, combined concurrent requests, retried transient failures, and returned copies so callers couldn't accidentally change the cached data.
Theme runtime
The theme turned a color pair into CSS properties and stored the choice for the session. Changing those properties recolored the page without replacing the content or reloading it. The day/night button also kept its accessible label and pressed state in sync.
function applyPair(pair) {
var root = document.documentElement;
root.style.setProperty("--color-1", rgbToHex(pair.primary));
root.style.setProperty("--color-2", rgbToHex(pair.secondary));
root.style.setProperty("--color-1-rgb", pair.primary.r + ", " + pair.primary.g + ", " + pair.primary.b);
root.style.setProperty("--color-2-rgb", pair.secondary.r + ", " + pair.secondary.g + ", " + pair.secondary.b);
root.style.setProperty("--theme-contrast", pair.ratio.toFixed(2));
}
function syncToggleState(button, mode) {
button.setAttribute("aria-pressed", mode === "night" ? "true" : "false");
button.setAttribute(
"aria-label",
mode === "night" ? "Switch to day mode" : "Switch to night mode"
);
}
Navigation model
I wrote a small router that fetched the destination page and replaced its <main> element. The shared navigation
stayed mounted. Before the swap, it stopped the old page's effects; afterward, it updated the metadata and started the new page.
This let me animate between complete static pages, although it also left me responsible for the details of that navigation.
async function navigate(urlValue, options = {}) {
const targetUrl = new URL(urlValue, window.location.href);
const route = routeFor(targetUrl);
const modulePromise = route && route.load
? route.load()
: Promise.resolve(null);
const [response, pageModule] = await Promise.all([fetch(targetUrl.href, {
credentials: "same-origin",
headers: { "X-Requested-With": "fetch" }
}), modulePromise]);
const html = await response.text();
const nextDocument = new DOMParser().parseFromString(html, "text/html");
const nextMain = nextDocument.querySelector("main");
await animateSwap(() => {
unmountPage();
document.querySelector("main").replaceWith(nextMain);
updateHead(nextDocument);
updateNavCurrent(targetUrl);
});
await mountPage(targetUrl, route, Promise.resolve(pageModule));
}
Unified search
Search combined the blog, project, and resume indexes. I normalized case, accents, and whitespace before matching so the same query worked across all three. The normalization function was shared rather than repeated in each data adapter.
export function normalizeSearchText(value) {
let text = String(value || "").toLowerCase();
if (typeof text.normalize === "function") {
text = text.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
return text
.replace(/[^a-z0-9+#./\s]/g, " ")
.replace(/\s+/g, " ")
.trim();
}
Interactive canvas system
This version's homepage picked a demo from a catalog of dynamically imported modules. It kept that choice during navigation, and a URL option could select a particular demo. The boids simulation used fixed time steps, with rendering interpolated between states so changes in frame rate didn't directly change the simulation speed.
function step(stepMs) {
stepSimulation(stepMs);
}
function render(interpolation) {
renderFrame(interpolation);
}
return createDemoHarness(canvas, {
simHz: FIXED_STEP_HZ,
init: onInit,
resize: onResize,
step: step,
render: render,
applyTheme: applyTheme
});
Deployment
The build produced a static artifact for GitHub Pages. Publishing followed repository pushes, so I didn't have an application server or a process manager to look after. That part of the setup was worth keeping in the later rebuild.
Related reading
Hello, world! was the launch post for this version. The site has since moved on: remi.works updated covers the current implementation.