Case Study —

iNKOBAN.net

Full-Stack Personal Website

Built without the use of frameworks - my own HTML, CSS, and JavaScript!

01Project Overview

Live URL inkoban.net
Role Sole Developer, Designer, Author
Timeline March 2025 — Present (actively maintained)
Frontend Vanilla HTML / CSS / JS (ES6+) — zero frameworks
Build 11ty v3 (Eleventy) · Nunjucks templates · Luxon
Backend Cloudflare Pages Functions · Workers KV
Hosting Cloudflare Pages · CI/CD via GitHub
Scope 7 pages · Blog · Portfolio · Guestbook · API layer

02Motivation

“I wanted to make my own website with my own hands to make something truly special. Doing it first-hand with vanilla web tech is something you need to do in order to learn.”

The Problem with Templates

I wanted to make my own special website. Most developer portfolio sites look identical, featuring nearly the same template, the same cards, the same dark-mode hero. They communicate technical competence but nothing about the person behind the site. While neat-looking, I felt as if I couldn't express myself or convey my personality through that standard way of building a site.

Building to Learn

There's a certain class of knowledge you can only acquire by doing the work yourself. I started this website with just three hours of HTML/CSS experience, and within two hours (and the magic of Flexbox), I'd built the first ever version. CSS layout, event delegation, browser rendering, serverless functions - these concepts click into place when you're the one fighting with them at 2am. Building iNKOBAN.net from scratch was a deliberate act of self-education.

The Retro Web Angle

I grew up on the early web - simple as! At some point, modern designs become repetitive and homogenized - vibecoded websites, even moreso - so I began looking to the past to sate my desires for authenticity. These old interfaces - phpBB forums, Flash portals, ancient directory pages - were functional, dense, and full of personality. Recreating them with modern browser capabilities became my own personal vehicle for mastering HTML, CSS, and JavaScript.

03Architecture

Frontend

HTML / CSS / Vanilla JS
  • Pure HTML5, CSS3, and ES6+ JavaScript, deliberately eschewing frameworks so I may learn the underlying platform and browser behavior from first principles rather than relying on abstractions
  • 5 global CSS files (style.css, navbar.css, home.css, and two alternatives) plus per-page stylesheets
  • CSS custom properties (design tokens) scoped per page for isolated theming
  • Vanilla JS draggable window system, tabbed property sheets, SPA-style navigation, and toast notifications - all handwritten!
  • Zero runtime dependencies on the client side

Zero dependencies for this project. I'd started with 3 hours of experience in HTML/CSS and only one (1) in JavaScript. Having gained more experience in these languages, I wanted to understand what React abstracts away before reaching for it. Every DOM manipulation in newshoes.js uses native APIs: querySelector, addEventListener, cloneNode, classList. The per-page theming works through CSS cascade ordering - where style.css loads globally, then navbar.css, then each page's own stylesheet last. Because page-specific rules load last and are scoped via a .page-* body class, they naturally win specificity battles without !important or resets. Each page defines its own set of CSS custom properties - the blog has 25+ tokens for its phpBB steel-blue palette, projects defines 30+ with --proj-accent: #ff6d00, portfolio has its warm tan and indigo scheme. Zero color leakage between pages - hopefully!

Build Layer

11ty + Nunjucks
  • Migrated from a flat static HTML/CSS/JS codebase to 11ty v3 (Eleventy) in April 2026
  • Nunjucks templates replace manual HTML duplication across pages
  • Targeted content authored in Markdown; data in plain JS objects (_data/)
  • Everything 'long-winded text' is now a .md file! Adding a project to Projects/ is adding a JS object to an array
  • Preserves the original hand-coded layouts.
  • Little "fiddly bits" like stats counters, view counts, and other small dynamic elements are now generated at build time or via the serverless API endpoints rather than being hard-coded into the HTML
  • Build output's a static HTML to _site/, deployed via CI

The 11ty migration was a constraint-preservation exercise. I had a working site with carefully crafted layouts, and I needed to wrap a build system around it without changing a single rendered pixel. Nunjucks templates replaced the copy-pasted navbars and footers, but the actual page markup stayed mine. The blog collection uses a two-tier sort I'm particularly pleased with, where the sticky posts bubble to the top via a frontmatter flag, then within each group, posts sort newest-first. Draft filtering checks ELEVENTY_RUN_MODE === "serve" rather than NODE_ENV, which I'd learned after discovering that 11ty never sets NODE_ENV itself. The musings collection is fundamentally different: it sorts by a manual order field, not date, because that content is curated by hand. A custom forumDate filter produces phpBB-style timestamps like "Sat Feb 01, 2026 4:20 pm" - and it accepts an optional time string to handle posts that don't have a meaningful time.

Backend

Cloudflare Pages Functions + Workers KV
  • Cloudflare Pages Functions provide serverless API endpoints co-located with the static site
  • Cloudflare Workers KV stores all dynamic state: page view counters, poll votes, guestbook entries, and online presence heartbeat
  • API routes: /api/counter/, /api/poll/, /api/views/, /api/online/, /api/guestbook/
  • No traditional backend server, no database to manage - and NO EXTRA BILL!
  • Zero cookies, zero third-party trackers - which I deliberately avoid in order to respect user privacy and keep the site lightweight and self-contained

All five endpoints share a single KV namespace (env.POLL_VOTES) with key prefixes doing the organizational work: poll:*, views:blog:*, counter:*:total, gb:entry:*, online:*:*. I considered separate namespaces but the prefix convention keeps things simple and the free tier limits one namespace anyway. The guestbook endpoint uses constant-time admin key comparison - XOR byte accumulation rather than early-return string equality - to resist timing attacks. Rate limiting hashes the cf-connecting-ip header with SHA-256 and stores it as a KV key with an 86,400-second TTL. When the TTL expires, the key auto-deletes, so I don't need to have a cron job to delete the expired rate limit keys. The online presence system works similarly: each visitor session gets a UUID stored as a KV key with a 600-second TTL. Counting online users is just counting keys under the online: prefix, so all the dead sessions evict themselves. View counter increments use a read-increment-write pattern with no transactional guarantees, which is fine for vanity metrics and explicitly not fine for anything financial.

Deployment

Cloudflare Pages + GitHub CI
  • Git push triggers an 11ty build on Cloudflare Pages
  • Functions deployed in the same pipeline as the static output
  • Branch previews available for every PR
  • Custom domain (inkoban.net) with Cloudflare DNS and edge caching

The deployment story is deliberately boring. A git push to main triggers an 11ty build on Cloudflare's infrastructure, outputs static HTML to _site/, and deploys it alongside the Pages Functions in a single atomic pipeline. Branch previews mean I can test a new blog post layout on a real URL before merging. The entire hosting cost is zero dollars (besides paying Cloudflare for the domain). I don't need to maintain nginx configs, manage a server, or run any other infrastructure for this site. Babysitting servers would be a huge time sink and a source of potential outages and maintenance headaches for a personal site...

04Design System

Each section of the site is its own little visual world! Each one is a complete, coherent period reconstruction, itended to slightly eschew a unified design system in favor of experimenting.

Page Visual Reference Key Techniques
Home Early 2000s web portal
(Generico Portal of late 2000s and early 2010s)
Switchable layouts (Portal / Yahoo JP / Classic), localStorage theme persistence (classic theme has windows!!)
About Me IE5 browser window
(stylized Windows 98/2000 era)
Draggable window chrome, faux address bar, in-page SPA navigation without page reload
Blog phpBB forum
(mid-2000s web forum)
Forum-styled long-form posts, live KV counters, polls, callout boxes, online heartbeat
Projects Flash Player 8 embed
(Xbox 360 blade tabs)
Pure-CSS radio-button tabbed navigation, project data driven by JS data files
Portfolio Pharmaceutical/Corporate print
(Wamdue Project, 1998)
CSS dot-grid background, CMYK registration marks, print-proof margin annotations
Other Yahoo! Japan directory
(2003 era web index)
Category-grid layout, changelog, colophon, site FAQ, recommended resources

Each page is its own period reconstruction - either stylized or deliberately authentic to the form. The blog recreates a phpBB forum from the mid-2000s, down to the exact fonts, colors, and border patterns. The About Me page recreates an IE5 browser window with a faux address bar, status bar, and window chrome. The home page offers three switchable layouts (Portal, Yahoo JP, Classic) stored in localStorage under homepage_layout, toggled by swapping the disabled attribute on <link> elements, which prevents any style evaluation overhead from disabled sheets. The Projects page uses pure-CSS tabbed navigation built from hidden radio buttons and sibling selectors. So - for all of these little "subsites," I treated each as their own standalone design problem with its own reference material, its own token set, and its own internal logic.

CSS Architecture

  • Global Win98 design tokens in :root (surface colors, shadow system, fonts)
  • Per-page scoped CSS custom properties override tokens without cascade conflicts, allowing each page to have its own theme/look without affecting other pages
  • Shadow system: --win-shadow-raised (3D button) and --win-shadow-inset (pressed state), which is very clickable, satisfying for the user.
  • No deeply nested selectors; max 3-4 levels throughout the codebase
  • Each page's style.css is self-contained and can be read as a standalone document

05Engineering Highlights

E01
11ty Migration
11ty · Nunjucks · Markdown · JavaScript Data Files
  • Migrated a fully hand-coded, multi-page static site to a templated build system without breaking a single existing layout or visual
  • Structured _data/ JS files replace repeated HTML: projects, changelog, links, and FAQ are now single sources of truth
  • Blog and musings collections are generated from Markdown frontmatter with custom sort logic, sticky-post support, and draft filtering
  • Build output is identical to the original HTML - the migration is invisible to the end user
E02
Serverless KV Persistence
Cloudflare Pages Functions · Workers KV · REST API
  • Designed and implemented a five-endpoint REST API layer using Cloudflare Pages Functions
  • Workers KV provides low-latency global persistence for counters, polls, guestbook entries, and real-time online presence
  • No traditional server provisioning, no database management (no fees!), and the entire backend fits in /functions/
  • Poll votes are deduplicated per session; view counts are tracked per-post slug

Writing a backend that fits entirely in a /functions/ directory forced a certain economy of design. All five endpoints talk to one KV namespace with prefixed keys (since I'd have to pay money for more!) with poll:* for polls, views:blog:* for page views, gb:entry:* for guestbook. I chose convention over configuration because I'm a bit too cheap to pay for premium.
The security model for the guestbook admin endpoint uses XOR-accumulated byte comparison for the admin key because even on a personal site, you've still gotta watch out for folks who want to clobber your credentials. Rate limiting is IP-based: the cf-connecting-ip header gets SHA-256 hashed and stored as a KV key with an 86,400-second TTL. When the TTL expires, the rate limit resets automatically.
The view counter uses Promise.all() for batch fetching multiple post counts in a single request, which matters when the blog index page needs counts for every post at once. The read-increment-write for counter updates has no transactional guarantee. I thought about this, decided visitor counters are one of the few things where eventual consistency is genuinely fine, and moved on.

E03
Vanilla JS Component System
ES6+ · DOM APIs · localStorage · IIFE modules
  • Handwritten draggable window management system with z-index stacking, focus tracking, and mouse capture
  • Pure-CSS tabbed property sheets using radio-button sibling selectors
  • SPA-style in-page navigation for the About Me section: content loads without page reload, URL-like state managed client-side
  • Modular IIFE architecture in a single newshoes.js file - no bundler, readable in DevTools
  • Toast notification system, homepage layout switcher with localStorage persistence, navbar toggle... all bespoke!
E04
Zero-Dependency Philosophy
Performance · Privacy · Maintainability
  • Two runtime dependencies total: 11ty (build-only) and Luxon (date formatting)
  • No JavaScript framework, CSS preprocessor, nor build bundler on the frontend - all me!
  • Zero cookies, zero analytics scripts, zero third-party trackers - very strong feelings about third party tracking!
  • View source in the browser and see exactly what runs

06Lessons Learned

Tech Debt

Starting this website with just three hours of experience in HTML/CSS was a disaster in terms of tech debt! My design philosophy and structure decisions early on created a lot of work to fix and refactor later as the site grew. You can see the remnants of such in the "Classic" theme! The visuals, too, were not consistent on the same page. I had to burn the midnight oil to go through each page and fix inconsistencies in spacing, typography, and other visual details to get things looking right. Tech debt! The JavaScript is also a bit of a mess from those early days, with some patterns and approaches that I would not use today but that I had to live with and refactor as the site evolved. 1500 lines of code is NOT what you want to see in one file.

CSS is actually a programming language

I came in thinking CSS was the easy part! I left knowing it deserves the same architectural discipline as any other system - with all the design tokens, cascade strategy, and specificity management. All of those things matter the moment you have more than two pages. The Win98 shadow system and per-page scoped variables are direct results of getting this wrong first.

Build systems earn their keep

The site launched as flat HTML files because I wanted to understand every byte. That was the right call early on... but by page four, updating the navbar meant touching six files. Migrating to 11ty proved to be an even greater hassle than trying to fix the byzantine code I'd made! Having to rewire the templates, partials, and data structure to get the site to build correctly with 11ty ended up being a huge undertaking. However, I deliberately wrote the blog code in a way that would make it easy to migrate into 11ty, so that part of the site would continue to work with minimal changes after the migration.

Serverless has a learning curve

KV is eventually consistent, which matters for view counters under concurrent load. Functions run at the edge, not in a traditional Node.js environment so certain APIs don't exist! However, the edge environment gives you other capabilities and constraints that you have to work with, and learning to reason about those constraints and the distributed nature of the system is part of the learning curve of serverless.

Shipping is the teacher

Nothing I read about web performance prepared me for the moment I checked my own page weight. Nothing I read about accessibility prepared me for screen-reader testing on my own markup. Shipping the site and seeing the results in the real world taught me lessons that reading alone never could. Just do things!