Tech stack
How GuerillaType is built. A static site, vanilla JavaScript, no frameworks, no backend, no database. Open source under MIT.
Site generator
Eleventy v3 (ESM mode) with Nunjucks templates and Markdown content. Why Eleventy:
- No client-side framework runtime. Pages ship as static HTML.
- ESM data files let us share types and helpers between build and runtime.
- A custom
.cssextension concatenates partials at build time and minifies via LightningCSS — no@importwaterfall. - HTML is minified through
@sardine/eleventy-plugin-tinyhtml. - Builds in ~320 ms cold for the entire site (~80 files, ~46 passthrough assets).
Frontend
- instant.page — ~1 KB script that preloads internal links on hover/touchstart so in-site navigation feels instant. No config; loaded once in
base.njk. - CSS — Vanilla, custom properties, no frameworks. ~37 partials concatenated into a single
global.css. Tokens defined intokens.css; all themeable values land there. - Typography — Lora for display, Inter for body, JetBrains Mono for the typing surface and code. Served by Bunny Fonts (privacy-friendly Google Fonts mirror, GDPR-compliant, no tracking).
- JavaScript — Native ES modules. No bundler. Imported directly via
<script type="module">. - No frameworks — no React, Vue, Svelte, Tailwind, jQuery. The total runtime JS is ~24 modules totaling under 30 KB before compression.
Typing engine internals
The engine has six small modules under src/assets/js/engine/:
| Module | Responsibility |
|---|---|
input-capture.js | Keystroke capture: keydown events, IME composition, paste-blocker, Backspace, Escape |
renderer.js | Span-by-span DOM diff for the typing surface, caret positioning, line-scroll |
metrics.js | Net wpm, raw wpm, accuracy, consistency (1 - coefficient-of-variation) |
typing-engine.js | Orchestrator: state machine, end conditions, results bundle |
adaptive.js | Per-character and per-bigram rolling model with Laplace smoothing |
wordpicker.js | Weighted roulette-wheel sampler over a 1k/5k/10k word list |
Data persistence
Everything in localStorage under tt:* keys:
tt:profiles— array of profile objects (name, settings, preferences, sessions, daily, hourly, perKey, perBigram, perFinger, perCharDetail, lessonResults, achievements, challengeBests, bookProgress, corpusProgress)tt:active-profile— currently active profile idtt:custom-texts— saved user-uploaded texts (each carries optionalmeta: { kind, sourceId, author, year, source, meaning }so corpus-page click-throughs can report completion back to the right item)tt:meta— schema version + migration recordtt:theme— light/dark preferencett:contribute-draft-<kind>— auto-saved form drafts for the eight contribute pagestt:testimonial-prompt-dismissed— single flag, set when user dismisses the post-session "leave a testimonial" prompttt:lesson-best-<id>— per-lesson personal bests (one key per cleared lesson)
Schema is versioned (currently v4) with forward-only migrations: v1→v2 adds perFinger, perCharDetail, lessonResults, sessionsByLesson, hourly, bookProgress, and a preferences subtree; v2→v3 resets whitespaceMark to none (the v2 default was too noisy); v3→v4 flips showVirtualKeyboard and keyboardFingerColors to true by default. corpusProgress is added lazily on read so existing profiles need no further migration.
Sessions are capped at 500 per profile (oldest dropped). Custom texts cap at 200 KB total per profile.
Visualizations
Five hand-rolled SVG components, no chart library:
- Keyboard heatmap —
<rect>per key from a layout matrix; fill interpolates from muted → accent → bad based on error rate or avg key time. - Trend line —
path d="M..."over the last 30 sessions, with min/max axis labels. - Contribution grid — 53 × 7
<rect>cells, each fill bucketed by daily practice minutes. - Per-key bar chart — top-12 horizontal bars sorted by avg key time.
- Progress ring —
stroke-dasharray = 2πr,stroke-dashoffset = 2πr * (1 − pct).
Hosting
- Cloudflare Pages — static asset hosting, free tier covers more bandwidth than this site will ever need.
- Cloudflare DNS — already free.
- Cloudflare Web Analytics — optional, privacy-friendly, no cookies.
- Umami — optional self-hosted alternative if you bring your own.
- Bunny Fonts — free CDN for my two web fonts. No Google Fonts.
Build tooling
@11ty/eleventy@^3@11ty/eleventy-plugin-rss@^2@sardine/eleventy-plugin-tinyhtmllightningcss— for production CSS minificationluxon— date filtersplaywright— viewport audit + smoke testsrimraf— cross-platformrm -rf
Performance budgets
| Asset | Budget | Notes |
|---|---|---|
| Critical HTML | < 12 KB gz | Above-the-fold content + inlined no-flash theme script |
global.css | < 25 KB gz | All partials, minified by LightningCSS |
| Engine bundle | < 18 KB gz | All engine/ modules combined |
| Total page weight (home, cold) | < 80 KB gz | Including font subsets |
Target: 95–100 PageSpeed across all four categories on /, /practice/, and /stats/.
License
MIT. See GitHub for the full text. Fork freely; attribution appreciated but not required.