← Articles

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.

Find content