Idea: client-side runtime render for /habits/ #

Status: not started, idea only. Nothing in this repo has been changed to
prepare for this. Written up so it can be picked up and evaluated properly in
a separate session, after weighing it against the plainer
eleventy-3-upgrade-plan.md fix for the same
underlying pain (full rebuilds being slow).

The pain this would address #

Every /habits/* page is fully rendered at build time: Nunjucks pulls
_data/habits/<slug>.json through _11ty/habits-data.js (loadHabits,
buildTimeline, stat aggregation) and bakes stat tiles, an inline SVG trend
chart (_11ty/habits.js's habitTrendChart), and monthly/daily tables
straight into HTML. Appending one day's Duolingo sessions to
_data/habits/duolingo.json and rebuilding forces the entire site through
Eleventy's full page-write path - confirmed at 562 pages / ~153s locally for
this repo's current state (2026-08-03), because every one of those pages
(not just the habit pages) runs the same expensive per-page transform chain:
JSDOM parse ×4 (apply-csp.js, img-dim.js, json-ld.js, the vendored
localimages transform) plus PurgeCSS and html-minifier (both in
_11ty/optimize-html.js). None of that is specific to habit data changing -
it's just what a full eleventy run does.

Two independent levers exist for this:

  1. Eleventy 1.x → 3.x upgrade (eleventy-3-upgrade-plan.md) - 3.x has a
    real incremental-build dependency graph that 1.x lacks, so a data-only
    change could in principle rebuild far fewer pages. Si has confirmed this
    is worth doing regardless of the incremental-build angle, for general
    maintenance and performance - it shouldn't need this doc's idea to
    justify starting it, and the two pieces of work should stay sequenced
    separately (upgrade first is probably easier to reason about, since it
    changes the tool underneath everything else).
  2. This idea - stop baking habit data into HTML at all. Ship a static
    page shell once, fetch pre-computed JSON at page load, and render
    client-side. This works whether or not the Eleventy upgrade happens, and
    is the only one of the two that also opens the door to interactivity
    (filtering, live search across habits) that static HTML can't do cheaply.

They're not mutually exclusive - the upgrade makes every page's rebuild
faster; this idea makes habit pages stop needing an HTML rebuild for a data
change at all. Worth deciding whether to do one, the other, or both, but do
the upgrade first and re-assess whether this is still worth the complexity
once it's done.

Verified facts relevant to feasibility #

Design sketch #

Keep _11ty/habits-data.js as the single source of truth for aggregation -
don't duplicate loadHabits/buildTimeline/stat math in client JS. Instead:

  1. Emit computed JSON at build time, one new template per shape needed,
    following the feed/json.njk pattern:
  2. Turn each habit page into a static shell: keep the static prose
    (description, source, the Duolingo QR code, the "only some rows have a
    level" caveat, etc. - all content that doesn't change per-entry) and
    replace the stat tiles / chart / tables with a mount point, e.g.
    <div id="habit-app" data-habit-url="/habits/duolingo/data.json"></div>.
  3. One small client script (bundled through the existing Rollup pipeline
  4. /habits/ index could similarly fetch /habits/data.json and render
    the habit-card grid + recent-activity timeline client-side, if it's worth
    doing there too - lower priority than the individual habit pages, since
    it changes less often per-entry (it's cross-habit).

Open decisions for whoever picks this up #

Suggested phased approach #

  1. Prototype narrowly on /habits/duolingo/ only (highest entry-frequency
    habit, so it's the best test of whether this actually removes rebuild
    pain) before touching /habits/sudoku/ or the /habits/ index.
  2. Add the /habits/duolingo/data.json output template; verify its output
    byte-for-byte matches what getHabit("duolingo") currently produces for
    the equivalent fields.
  3. Port habitTrendChart to a small, dependency-free browser module; unit
    test it against a few fixture habits (reuse test/ conventions already
    in this repo) rather than only eyeballing it in a browser.
  4. Build the shell + mount script for /habits/duolingo/, decide the
    progressive-enhancement question above, and manually verify: page loads
    with JS disabled (fallback content, if any, is sane), CSP has zero
    console violations, and Lighthouse/CLS doesn't regress badly from content
    popping in after fetch.
  5. Only then decide whether to extend to /habits/sudoku/ and the
    /habits/ index, or stop at the one-habit prototype if the trade-offs
    don't feel worth it.

Effort estimate #

Small-to-medium. The JSON-emission templates are a few lines each (precedent
already exists in feed/json.njk); most of the effort is the
habitTrendChart port and getting the progressive-enhancement fallback
right without regressing CLS/perceived load. Meaningfully smaller than the
Eleventy 3.x upgrade, but do that first and revisit whether this is still
wanted.