Fully migrated off Squarespace with the subscription cancelled. Grows with ordinary maintenance as new projects ship.
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.
Design, build, platform migration, and ongoing content, end to end.
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
| Piece | What it does |
|---|---|
| Pages | Every route is a plain <slug>/index.html file (About, Projects, 404, and one folder per project), so URLs stay clean with no .html extension. New pages start from a scaffold script, and old Squarespace URLs redirect through stubs generated from one redirect list. |
| Design System | Every interior page runs on kl-ui-v2.css (.klv2-page), with site.js for shared behavior. The homepage is the one deliberate exception, kept fully isolated with its own CSS and JS and no page system wrapper at all. |
| Build | A Node script with no dependencies writes the deployable site. It renders each page's head from pages.json, its breadcrumbs from the same titles, and the nav, footer and "Keep Exploring" cards from one registry file straight into the HTML, so they work with JavaScript off. Every CSS and JS reference gets a content hash, so a changed file is never served stale, and only an allowlist of files ships. |
| Checks | GitHub Actions runs the build on every push and pull request, then a link and markup check and an axe-core accessibility check over every page. Any new accessibility finding fails the run and blocks the deploy. A daily run checks the live site and reports to the admin hub. |
| Hosting | GitHub Pages serves the built site, deployed by Actions from the main branch of a private repo. Cloudflare (free plan) runs the DNS, proxies the site, redirects HTTP to HTTPS, and adds its real security headers. |
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.
| Area | Pages on kl-ui-v2 |
|---|---|
| admin.kevinlewis.net | Homepage, /status, and the entire Bob-a-Bot web surface (index, admin panel, health/live pages). |
| kevinlewis.net | Pi-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 hub: links to every project's live, admin, or status page, grouped by area, plus the dashboards and tools behind this site. 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.
| Piece | What it does |
|---|---|
| Hosting | A 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. |
| Identity | Gated by an AWS Cognito Hosted UI user pool with self-registration off and required MFA (authenticator app only). 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. |
| Status & Analytics | Live reachability and response times for each site, plus pageview analytics (24 hours, 7 days, 30 days, all time, top pages, and referrers). This site reports views with a small beacon: no cookies and no visitor ID. The public collect endpoint is rate limited, size capped, and sanitized. |
| Service Health | A Hydration Station dashboard that checks its Lambdas, DynamoDB table, and schedule directly. It reads AWS through the server's IAM role, so no access keys are stored anywhere, and each check fails on its own without taking down the page. |
| Reference | A domains table covering every serving host and its Cloudflare mode, plus the mail records (MX, SPF, DKIM, DMARC), checked against live DNS. Also the design system reference that every page here is built from. |
| Private Tools | A few private personal tools that live behind the same login. |
| Security | A dedicated security review fixed 10 findings, including two high severity crash bugs reachable without logging in. The server sets its own security headers (Referrer-Policy, Permissions-Policy, and a frame-ancestors CSP), matching the main site. |
| Deploy | One command copies the build to the server and restarts it under pm2. |
Tools + Build
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, One Small Build. 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, so the build is one Node script with no dependencies that fills in the shared pieces and asset hashes and nothing more.
Registry-Driven Nav, Footer, and Related Projects. Rather than hand-editing a nav block on every page, the nav, footer, and "Keep Exploring" suggestions are built into every page from one registry file, so adding a link or a project happens in one place instead of 25-plus, and the links work with JavaScript off.
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 as a Meta Tag, the Rest From Cloudflare. 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. Some protections have no meta equivalent at all (frame-ancestors, X-Frame-Options, X-Content-Type-Options, Permissions-Policy), so the domain's DNS moved to Cloudflare's free plan and a Transform Rule adds them as real headers. The securityheaders.com grade went from C to A+, with no server added.
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.
| Step | What happened |
|---|---|
| Rebuild | Every 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. |
| DNS | Squarespace'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. |
| Verify | Before 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. |
| Cutover | Domain 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
| Aspect | Detail |
|---|---|
| Hosting | $0 a month. GitHub Pages hosting and Cloudflare's free plan replace a paid Squarespace plan. |
| Unaffected | Domain registration and Google Workspace email were never billed through Squarespace, so cancelling it did not touch either. |
| Ongoing | Time, 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.