projects

remi.works

Feb 15, 2025

Technical Overview

remi.works is a static, content-driven portfolio system deployed on GitHub Pages. The site is intentionally built with plain HTML, CSS, and modular JavaScript so pages remain lightweight, inspectable, and easy to ship, while still supporting richer behavior such as animated navigation, theming, search, and interactive canvas demos.

Architecture

  • Static routes define the core surface area: index.html, blog.html, projects.html, project.html, and related pages.
  • Structured metadata lives in JSON indexes: data/posts.json, data/projects.json, and data/resume.json.
  • Long-form content is stored as HTML fragments in blog/posts/*.html and projects/details/*.html.
  • Page responsibilities are split into focused modules under js/*.js to keep logic isolated and reusable.

Content Pipeline

Blog posts and projects are loaded from indexed metadata and resolved by slug at runtime. The loader memoizes successful responses and coalesces in-flight requests, which reduces duplicate network work and keeps rendering deterministic across navigation paths.

async function loadProjects() {
  if (Array.isArray(projectsCache)) {
    return projectsCache.slice(0);
  }

  if (projectsRequest) {
    var pending = await projectsRequest;
    return pending.slice(0);
  }

  projectsRequest = fetch(PROJECTS_URL, { cache: "no-store" })
    .then(function (response) {
      if (!response.ok) {
        throw new Error("Failed to load projects index");
      }
      return response.json();
    })
    .then(function (items) {
      projectsCache = normalizeProjects(items);
      return projectsCache;
    })
    .finally(function () {
      projectsRequest = null;
    });

  var loaded = await projectsRequest;
  return loaded.slice(0);
}

Theme Runtime

The theme system maps color pairs into CSS custom properties and persists user choice in session storage. This allows instant palette updates and contrast-aware styling without rebuilding the DOM or forcing a full page refresh.

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 setToggleSymbol(button, mode) {
  button.textContent = mode === "night" ? "☾\uFE0E" : "☀\uFE0E";
  button.setAttribute("aria-pressed", mode === "night" ? "true" : "false");
}

Navigation Model

Navigation uses an MPA shell with selective in-page replacement. Internal links are intercepted, the destination document is fetched, and only the <main> subtree is swapped while shared chrome remains mounted. This preserves context and enables smoother page-to-page transitions without introducing a full SPA framework.

function navigateTo(url, options) {
  var targetUrl = new URL(url, window.location.href);

  return fetch(targetUrl.href, {
    credentials: "same-origin",
    headers: { "X-Requested-With": "fetch" }
  })
    .then(function (response) {
      if (!response.ok) {
        throw new Error("Failed to fetch page");
      }
      return response.text();
    })
    .then(function (html) {
      var parser = new DOMParser();
      var nextDoc = parser.parseFromString(html, "text/html");
      var nextMain = nextDoc.querySelector("main");
      var currentMain = document.querySelector("main");

      currentMain.replaceWith(nextMain);
      updateHead(nextDoc);
      updateNavCurrent(targetUrl);
    });
}

Unified Search

Search spans blog, project, and resume data through a shared normalization path. Text is lowercased, de-accented, and tokenized so matching behavior is consistent even when source documents come from different schemas and writing styles.

function normalizeText(value) {
  var 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

The homepage boids demo uses a fixed-step simulation loop with interpolated rendering. Physics updates run at a stable cadence, while draw calls interpolate between states for smoother animation under variable frame times. Boid silhouettes are defined by constant geometry values so the visual language stays flat and intentionally 2D across devices.

var BOID_HEAD_LENGTH = 5.8;
var BOID_TAIL_LENGTH = 3.5;
var BOID_WING_OFFSET = 2.5;

function tick(ts) {
  var simStepMs = lowPowerMode ? lowPowerSimStepMs : normalSimStepMs;
  accumulatorMs += deltaMs;

  while (accumulatorMs >= simStepMs && steps < maxStepsThisFrame) {
    stepSimulation(simStepMs);
    accumulatorMs -= simStepMs;
    steps += 1;
  }

  renderFrame(simStepMs > 0 ? accumulatorMs / simStepMs : 1);
}

Deployment

Delivery is handled by GitHub Pages. The repository publishes as a static artifact, so deployment is branch-driven and operational overhead stays low: no app server, no process manager, and no runtime infrastructure to maintain.

Related Reading

For launch context and rationale, read the companion blog post: Hello World!