Skip to content
The Level Up Manual

Devlog

How this book was built

A book about evidence should show its work. These entries record the decisions that shaped this site — what changed, why, and what the reader sees as a result.

2026-08-19

Eight Hours, Two Fewer Things

"Simplify the home page." That was the assignment. Eight hours later, the page has a privacy badge, a hue that shifts with the clock, and two fewer visible things than it could have had. This is the story of how that happened, and it is not a story about restraint. Not at first.

We already told you about the declutter: the hero contents box came out, the four parts of the book moved into the nav as an accordion, and every element left on the page had to answer a question the reader actually asked (commit 14400ef). Then the scorecard push landed - knowledge checks, themes, badges, offline support, even this devlog - and the home page started feeling like the plain cousin at a loud party. A reader said it still read as boring. Fair. So we asked the dangerous question: how do we make it feel alive?

That question produced a design brief with roughly twenty-six answers. A dynamic hero momentum card. A visual pillar constellation. Micro-interactions, scroll-triggered typography, a breathing background gradient, an activity heatmap, an inline quiz widget, an XP header, a time-of-day aesthetic shift, a privacy badge. All of it local-first by construction: the site is a Next.js 16 static export to GitHub Pages (basePath /LevelUp), so anything "alive" has to come from the browser's own localStorage - keys like levelup-streak-v1 and levelup-quiz-v1 - zero APIs, zero telemetry.

Then came the QuoteCard.

  • Commit 6601138 added it: a React 19 client component that fetches /data/quotes.json on mount, picks a random quote, and renders it with a "Read chapter" link. Locally: build green, 22/22 tests, eslint clean.
  • GitHub Actions failed eight times in a row. The same phantom TypeScript error every run: TS7006, parameter 'array' implicitly has an 'any' type.
  • The spiral: a type fix, a @ts-ignore, a @ts-nocheck, then empty commits to force re-runs. The commit messages tell the story honestly: "force CI re-run", "another CI trigger", "force CI re-re-re-run". Every re-run failed identically while the same tree passed locally.

It was never a code bug. It was the environment disagreeing with us, and it ate a day before we accepted that.

What actually shipped was restraint (commit 13e3391): a PrivacyBadge -"100% Private · On-Device Storage Only", shown once per session after two seconds - and a ThemeTimeShift, a tiny client component that reads the clock once on mount and nudges the --hue CSS variable from crisp and bright in the morning toward moody and calm at night. Two small things. The 26-idea brief was cut to four priorities, and only two shipped. The quote card that cost us a day of CI is not even on the page anymore; it was deleted in commit 13e3391.

Eight hours, two additions, and two fewer things than we could have shipped. We are prouder of what we did not build than of what we did. The rule: subtraction is a loop, not a one-time cleanup - and stopping, with the brief still on the table, is the feature.

2026-08-19

Under the Hood: A Static Site That Thinks

A book about evidence should show its work — and this site is no exception. Here is what happens under the hood when you open a chapter, search, or adjust your reading experience.

Local storage, the only "server"

Every dynamic thing on this page derives from the browser's own storage. No APIs, no telemetry, no third-party scripts. The key namespaces live under levelup-*:

  • levelup-streak-v1 — current/best streak, last date
  • levelup-quiz-v1 — per-chapter quiz scores {slug: {score, total, ts}}
  • levelup-highlights-v1 — array of {id, slug, quote, createdAt}
  • levelup-reflections-v1{slug: text}
  • levelup-progress-v1{slug: {complete, maxScroll, updatedAt}}
  • levelup-reader-scale1, 0.85, 1.15, or 1.3
  • levelup-reader-lh — line-height preset: tight/normal/airy
  • levelup-reader-font — font family: source/serif
  • levelup-reader-contrasttrue enables a higher-contrast token set

Read/write is orchestrated via useSyncExternalStore in lib/activity.ts and lib/progress.ts: a store subscribe/emit pattern that keeps React cellular and persisted across sessions. The core contract is in addHighlight, saveQuizResult, recordActivity, and markComplete — each reads the current snapshot, mutates it, JSON-stringifies it, and writes it back. If window.localStorage is absent (e.g. private browsing), the fallbacks are empty arrays/objections so the UI degrades gracefully.

// From lib/activity.ts — highlight storage snapshot
function readJson<T>(key: string, fallback: T): T {
  try {
    const raw = window.localStorage.getItem(key);
    return raw ? (JSON.parse(raw) as T) : fallback;
  } catch {
    return fallback;
  }
}
// From lib/search.ts — MiniSearch engine bootstrap
import MiniSearch from "minisearch";

export async function getSearchEngine() {
  if (engine) return engine;
  const base = process.env.NEXT_PUBLIC_BASE_PATH || "";
  const res = await fetch(`${window.location.origin}${base}/data/search-index.json`);
  const docs = (await res.json()) as Doc[];
  engine = new MiniSearch({
    fields: ["title", "sub", "teaser", "text"],
    storeFields: ["id", "type", "title", "sub", "teaser", "url"],
    searchOptions: {
      boost: { title: 4, sub: 2, teaser: 1.5 },
      fuzzy: 0.2,
      prefix: true,
    },
  });
  engine.addAll(docs);
  return engine;
}

How search works

When you press ⌘K (or click the magnifying glass), a Modal opens and fetches the prebuilt index at public/data/search-index.json (generated at build time by scripts/build-data.mjs). MiniSearch initializes on first open and is cached for the session. Typing ≥2 characters triggers engine.search(query) with fuzzy matching, prefix support, and title/sub/teaser boosting. Results appear instantly — the entire index is ~30KB and lives in the browser.

Reader customization

Three knobs live in local storage and affect the .book-prose root:

  • Font size (A−/A+): scales font-size via --reader-scale (85–130%).
  • Line height (tight/normal/airy): sets data-reader-lh, which maps to line-height in globals.css.
  • Font family (serif/sans): sets data-reader-font, which maps to font-family in globals.css.
  • High contrast (toggle): sets data-contrast="high", which swaps the colour palette in globals.css for stronger ink/line contrast and neutralizes muted text-ink-faint roles.

All four values persist across sessions and are honored on page load without a round-trip.

How this was made (AI transparency)

The raw text of the twenty-eight chapters was originally processed by an LLM to extract key concepts, evidence grades, and protocol references — roughly 20 000 lines of transcript compressed into structured data. The LLM also helped clean up syntax and format the devlog entries you're reading now.

The architecture, UI, state layer, Navigation accordion, search indexing, build pipeline, and deployment workflow are all mine. I wrote the localStorage handlers, structured the nested accordion menu, built the reading progress tracker, and saw the project through from static export to GitHub Pages deploy. The LLM was a utility for compressing raw transcript text into structured chapters — not a substitute for hands-on code ownership.

Demo

<video controls autoplay loop style="max-width: 100%; height: auto;"> <source src="/devlog/demo-reading-flow.mp4" type="video/mp4"> Your browser does not support the video tag. </video> <video controls autoplay loop style="max-width: 100%; height: auto;"> <source src="/devlog/demo-theme-switcher.mp4" type="video/mp4"> Your browser does not support the video tag. </video>

(Videos are short GIF/MP4 exports of the reading flow and theme/typography switcher captured via headless Chrome + ffmpeg. They are not embedded in the live deployed site but appear in this devlog for transparency.)

2026-08-17

The Scorecard Push

Every chapter used to be a wall of text with a scroll bar. This update turned the reading experience into something a reader can do things with:

  • Knowledge checks — each chapter ends with three questions drawn from its own key concepts, evidence grades, and protocols, plus a reflection prompt. No API, no accounts: the questions are generated deterministically at build time and the answers are checked locally.
  • Text highlighting — select a passage, mark it, and it stays highlighted.
  • A scroll-spy table of contents — the chapter sidebar now follows you down the page.
  • J and K keyboard shortcuts — flip between chapters without touching the mouse.
  • Group banners — the four parts of the book announce themselves at chapters 1, 12, 16, and 21.
  • Backup & restore — export your progress, highlights, quiz scores, and reflections as a JSON file; import them on any other device.
  • Levels and badges — local gamification with honest rules: XP for chapters, quizzes, highlights, and reflections; nine badges from First Step to Quiz Master.
  • Reader themes — Light, Dark, Deep Work (low-glare sepia), and Cyberpunk.
  • Offline support — a service worker precaches the shell and data so the book works without a connection.
  • A devlog — this page, because a book about evidence should show its work.

Everything is derived from real chapter data, everything is local-first, and nothing phones home.

Before and after

The home page before this push (the first version shipped with the book) and after the declutter:

Home page before the declutter

Home page after the declutter

2026-08-17

Why the Home Page Got Simpler

The first version of this site crammed a lot onto the home page: hero copy, a big "Contents" box that listed the book's four parts, a "how to read" section, stats, and three call-to-action buttons. It looked like a pilot's cockpit — dense, impressive, and hard to know where to start.

A reader gave feedback that landed hard: the page was trying too hard, and the contents box made the table of contents feel like a rerun. The fix was subtraction:

  • Removed the hero contents box entirely. The chapters grid still lives one click away, and the four-part grouping moved into the navigation itself.
  • Replaced the nav's chapter list with an accordion menu (Table of Contents) that collapses the four parts of the book — Build the Belief Engine, Move to Action, Train Body & Mind, Build the Business — so the 28 chapters stop intimidating before you click.
  • Kept the stats row and the "how to read" section, because both answer the real questions: is this verified? and how long will this take?

The rule that came out of it: every element on the home page must answer a question the reader actually asked. If it doesn't, it gets cut.

2026-08-16

Plain English, Same Evidence

The book started as a dense distillation of twenty-eight self-development trainings. Dense is good for reference; it is bad for reading. So every chapter went through a plain-English pass with three hard rules:

  • Quotes stay verbatim. If the source said it, we do not paraphrase it.
  • Grades stay honest. Every A–D evidence grade survived untouched. The audit page still shows exactly why each claim got what it got.
  • Numbers and protocol references stay exact. Protocol 2.7 is still 2.7.

Twenty-two of the twenty-eight chapters changed in some way; six were already plain enough. Across the whole book, the word count stayed nearly identical (44,212 → 44,141) — this was a rewrite for clarity, not for length.

The style target was simple: if a sentence needed reading twice, it got rewritten once.