Portfolio Website

This site itself: a static, framework-free build with a shared registry-driven nav, footer, and related-projects system, now migrating page by page from its original design system onto a second-generation one with dark mode. Originally a Squarespace site with each page pasted in as a Code Block, now a plain git repo of HTML, CSS, and JavaScript served from GitHub Pages.

Status Live, Actively Maintained

Fully migrated off Squarespace with the subscription cancelled. Grows with ordinary maintenance as new projects ship.

Build Type Solo

Design, build, platform migration, and ongoing content, end to end.

Primary Focus

Static-site architecture, a reusable design system, and an in-progress design-system migration.

Project Overview

Purpose: One consistent design system and workflow for adding new project pages and content quickly, without fighting a page builder or carrying a recurring subscription.

Problem: Squarespace's workflow meant pasting HTML, CSS, and JS by hand into a Code Block on every page: no git history, no real diffs, and a monthly bill for hosting a site that doesn't need a page builder at all.

Approach: Move to GitHub Pages: every page becomes a real file in a git repo, deployed by a push, sharing one design system and a small set of registry-driven scripts instead of copy-pasted markup.

Where the Homepage's Personality Came From

The homepage's tile hero, hover-driven Kevin spritesheet, and KevinOS terminal trace back to a long-time favorite: Jesse Schell's personal site. Schell led the creation of Disney's Toontown Online, a childhood favorite, before founding Schell Games and writing The Art of Game Design. His homepage swaps a conventional nav bar for a single illustrated, sleeping version of himself; interacting with it wakes him up and reveals speech-bubble links out to his book, studio, and talks.

Reference: jesseschell.com replaces a standard nav with one character whose state changes on interaction, surfacing links as speech bubbles instead of a menu bar.

Applied Here: kevinlewis.net's homepage borrows the same idea: a 9-frame Kevin spritesheet and KevinOS terminal that respond to hover instead of sitting still, turning the hero into something explorable rather than decorative.

How It Works

Site architectureNo build step, no bundler, between editing a file and it going live.
PieceWhat it does
PagesEvery route is a plain <slug>/index.html file (About, Projects, 404, and one folder per project), so URLs stay clean with no .html extension.
Design SystemInterior pages run one of two shared CSS/JS pairs: the original kl-design-system.css (.kl-page) or the newer kl-ui-v2.css (.klv2-page), migrating page by page. site.js dual-supports both. The homepage is the one deliberate exception, kept fully isolated with no page-system wrapper at all.
RegistriesThe top nav, the footer, and each project page's "Keep Exploring" suggestions are all driven by small JS registries instead of markup repeated across 25-plus pages. Add one entry once and every page picks it up on load.
HostingStatic files served straight from the main branch of a private repo, with a CNAME file and Pages' custom-domain setting pointing kevinlewis.net at GitHub's edge, HTTPS enforced.

kl-ui-v2: The Second-Generation Design System

A second design system, built additive to the original rather than replacing it: same tokens, fonts, and header, plus opt-in dark mode, a sticky-aside layout with scroll-spy for content-dense pages, accordions, tabs, tables, an item-grid-plus-carousel pattern, and a handful of components ported over from v1 (status cards, quotes, timelines, the image lightbox, a Ken Burns photo crossfade, video embeds). The rollout is opportunistic, not big-bang: pages move to v2 as they're touched, not all at once. The homepage stays untouched by policy either way.

Migration progressPages already moved, roughly in the order they were converted.
AreaPages on kl-ui-v2
admin.kevinlewis.netHomepage, /status, and the entire Bob-a-Bot web surface (index, admin panel, health/live pages).
kevinlewis.netPi-Tether, Projects, About, Twitch Stream Toolkit (plus its Admin Panel sample), Pin-Sighter, Plunko, Hydration Station, Current & Future Projects, and this page.

Real Bugs the Migration Surfaced

Two Timers Fighting Over the Same Hero Photo

The About page's Ken Burns photo crossfade and a separate generic image-cycle helper both bound independent intervals to the same hero image, since the crossfade's inner image happened to match the generic helper's selector too. The two timers fought over the image's src, opacity, and transform on staggered periods, producing visible random jumps instead of a smooth cycle. Fixed by excluding crossfade-owned images from the generic helper's selector.

Doubled Paragraph Spacing in the Aside Layout

The sticky-aside layout's grid already adds a gap between elements; consecutive paragraphs inside it also carried their own default margin, so the two stacked into noticeably oversized gaps. Fixed by zeroing paragraph margin inside that layout so the grid gap is the only source of spacing.

A Linked Feature Card With No Underline Reset

The feature-card component had only ever been used as a plain <div>, so it carried no text-decoration reset. The first page to use one as a real link rendered every word inside it underlined. Fixed at the component level so any future linked card works correctly too.

Admin Hub

A separate subdomain hosts a private links hub to every project's live, admin, or status page, grouped Live, In Progress, and Planned. It's architecturally distinct from the main site: not GitHub Pages, but a small zero-framework Node server deployed to the same always-on EC2 box as Bob-a-Bot, behind a Caddy reverse proxy handling automatic HTTPS.

Admin hubA separate app, not part of the GitHub Pages build.
PieceWhat it does
HostingA zero-framework Node server built on the built-in http and crypto modules, deployed via scp and pm2 alongside Bob-a-Bot on the same EC2 box, with DNS pointed at it via an A record rather than a GitHub Pages CNAME.
IdentityGated by an AWS Cognito Hosted UI user pool with self-registration off and required MFA. The server runs the OAuth2 authorization-code exchange, verifies the returned ID token against the pool's JWKS, and sets an HMAC-signed session cookie, no passwords or session state handled by the server itself.

Tools + Build

HTMLCSSVanilla JavaScriptCSS Custom PropertiesDark ModeIntersectionObserverGitHub PagesGitGitHubGoogle FontsOpen SansJetBrains MonoContent Security PolicyDNS (A Records)

Decisions Worth Calling Out

GitHub Pages Over Squarespace. Moved off a paid page-builder subscription onto free static hosting already used for other projects, trading a visual editor for git-based version control, real diffs, and a push-to-deploy workflow.

No Framework, No Build Step. Every page is plain HTML, CSS, and JS behind a shared stylesheet and script. For a mostly-static content site, a framework and bundler would add tooling without solving a real problem here.

Registry-Driven Nav, Footer, and Related Projects. Rather than hand-editing a nav block on every page, the nav, footer, and "Keep Exploring" suggestions render at runtime from three JS registries, so adding a link or a project happens in one place instead of 25-plus.

Opportunistic Migration, Not a Rewrite. kl-ui-v2 rolls out page by page as each one is touched anyway, rather than a single big-bang rewrite of the whole site. Both systems stay live side by side indefinitely; site.js dual-supports both so a page can move on its own schedule with no coordination cost.

CSP Delivered as a Meta Tag. GitHub Pages serves static files with no way to set custom HTTP response headers, so the Content-Security-Policy ships as a meta tag in every page's head instead of a real header, the only mechanism available without adding a server or CDN layer.

Clean Slug URLs Over .html. Every route is <slug>/index.html rather than <slug>.html, so links read as /projects/pi-tether instead of carrying a file extension.

Self-Hosted Images, No Hotlinking. All images live in the repo's own assets folder rather than pointing at another repo or CDN, so the site never depends on an external source staying online or public.

Squarespace → GitHub Pages

The site ran on Squarespace for years with its built-in navigation disabled and every page rebuilt as a full-page Code Block. The migration replaced that entirely: no paste step, no subscription, no page builder standing between an edit and the live site.

Platform migrationSquarespace to a plain git repo on GitHub Pages.
StepWhat happened
RebuildEvery page converted from a pasted Code Block into its own <slug>/index.html, plus a from-scratch homepage rebuild since it was Squarespace's native block editor, not a Code Block, so nothing could be reused directly.
DNSSquarespace's own DNS Settings dropped the Squarespace Defaults A records and added GitHub Pages' four A records for the apex domain, leaving Google Workspace email untouched.
VerifyBefore cancelling Squarespace: every page checked for a 200 response, every referenced image checked for resolving, a console-error sweep, and a grep for any remaining Squarespace reference across every served file.
CutoverDomain registration and Google Workspace email were never billed through Squarespace, so cancelling it left both untouched. Only the recurring site-hosting subscription went away.

Notable Challenges

A few problems surfaced only because the migration forced a genuinely close look at what was actually deployed, not just what looked right locally.

Fixes That Never Left Scratch Work

Symptom: Cancelling Squarespace forced a full recheck of the deployed files, which surfaced that a few features built earlier in the migration, including an image lightbox and a photo crossfade, had only ever existed in in-session scratch copies, never the actual committed files.

Fix: All of them were rebuilt from scratch and confirmed live, not just locally, which is now the standing rule for this project: verify against what GitHub Pages is actually serving, not the local working copy.

A Dropdown That Closed Before the Mouse Arrived

Symptom: The Interactive Projects nav dropdown closed before a real mouse path could reach an item inside it.

Fix: A CSS-only gap-bridge fix helped but wasn't reliable against real mouse movement, replaced with a JS grace-period timer keyed off the same class the mobile tap-toggle already used.

Stale DNS Cache Looked Like a Failed Cutover

Symptom: After repointing DNS, the apex domain still resolved to the old Squarespace answer well after the change went through.

Fix: The local machine's DNS resolver was serving a stale cached answer; querying a public DNS resolver directly confirmed the change had actually propagated.

Cost

What changedMoving off a paid page builder.
AspectDetail
Hosting$0 a month. GitHub Pages hosting is free, replacing a paid Squarespace plan.
UnaffectedDomain registration and Google Workspace email were never billed through Squarespace, so cancelling it did not touch either.
OngoingTime, not money. The only ongoing cost is maintenance time, writing new project pages and keeping the design system consistent, the same as before, just without a recurring bill attached to it.

What's Next

  • Finish the opportunistic v2 rollout. Remaining interior pages move to kl-ui-v2 as they're next touched, not on a fixed schedule.
  • Ongoing content. New project pages and image updates as new projects ship, the same maintenance rhythm as before the migration.

More projects like this

← Back to all projects