Skip to content

Add discoverability, dark mode, search, social cards, banner grammar, and learner analytics#7

Merged
adewale merged 4 commits into
mainfrom
claude/project-elevation-opportunities-ytuzco
Jul 2, 2026
Merged

Add discoverability, dark mode, search, social cards, banner grammar, and learner analytics#7
adewale merged 4 commits into
mainfrom
claude/project-elevation-opportunities-ytuzco

Conversation

@adewale

@adewale adewale commented Jul 2, 2026

Copy link
Copy Markdown
Owner

What

Ships the distribution and product-polish pass from the project-elevation review: SEO plumbing (sitemap, JSON-LD, robots.txt), dark mode + skip link, client-side example search, per-example social-card images, the banner position grammar from docs/visual-explainer-spec.md (with curated multi-figure pairs), full journey coverage of all 109 examples, and a learner-behavior report over the existing observability events.

Why

The project's internal quality machine is saturated (per docs/rubric-saturation.md) while its outward distribution surface was nearly empty: no sitemap, no structured data, no search, no dark mode, no social-card images, and no view of what learners actually do. This PR converts existing investments (the marginalia figure set, the wide-event observability, the quality registries) into visitor-facing and author-facing value. The banner grammar was already specced and prototyped (/prototyping/layout-banner-*) but never rolled out.

How

  • SEO: a /sitemap.xml route in app.py, public/robots.txt with a Sitemap directive, and JSON-LD (WebSite on home, TechArticle/LearningResource on examples) injected through a new _layout parameter. scripts/lint_seo_cache.py now enforces all of it.
  • Dark mode: a prefers-color-scheme: dark block overriding the existing CSS custom properties. Shiki moves to dual themes (CSS-variable swap); CodeMirror picks oneDarkHighlightStyle via matchMedia. Marginalia figures render on a light "paper" chip in dark mode, so the locked SVG grammar is untouched.
  • Search: scripts/build_search_index.py emits a JSON index (title/section/summary/notes text, lowercased at build time) that goes through the existing fingerprint pipeline; public/search.js is a dependency-free ranked matcher with a / shortcut and keyboard navigation. Chosen over a search library to keep the zero-dependency asset story.
  • Social cards: scripts/build_social_cards.py composes a 1200×630 HTML card per example around its curated marginalia figure; scripts/build_social_cards.mjs rasterizes all 110 through one headless-Chrome CDP session to public/og/<slug>.jpg (JPEG q90 — 7.2 MB total vs 17 MB PNG). Pages reference them via og:image/twitter:card.
  • Banner grammar: render_banner(slug, position) implements before / after-cell-N (legacy anchor cell-N) / after-walkthrough, with multiple figures per position rendering as one small-multiple banner. ATTACHMENTS stays the single registry — extending its anchor vocabulary (instead of the spec's proposed parallel BANNERS dict) keeps all five contract families intact. Curated pairs: mutability (aliasing vs frozen tuple), positional-only-parameters (/ and * twins), metaclasses (both triangles), tuples (frozen tuple vs growing list), plus the one-pass caret on iterator-vs-iterable's exhaustion cell.
  • Journeys: the five previously unreferenced examples joined their natural sections with outcome-registry support updates; stale "gap placeholder" meta copy removed.
  • Learner analytics: scripts/learner_report.py consumes exported wide events (auto-detects raw payload, wrangler tail, and Workers Logs envelopes) and reports most-read pages, most-run examples with edited/error shares and execution percentiles, journey traffic, and /examples/ 404s. Documented in docs/learner-analytics.md.

Testing

  • 26 new tests across tests/test_app.py (Discoverability, DarkModeAndAccessibility, Search, SocialCard, Banner suites) and tests/test_learner_report.py; the geometry anchor contract now accepts the position grammar.

  • scripts/check_search_ranking.mjs (new make search-ranking-test, wired into make verify) exercises ranking against the real generated index.

  • Full suite: 120 tests pass, plus SEO/cache lint, example verifier (100% golden parity), all nine quality checks, formatter, and ruff.

  • New assertions fail without the features (e.g. removing a pair figure fails BannerTests; removing JSON-LD fails the linter and DiscoverabilityTests).

  • Not run locally: make browser-layout-test needs a local Worker and the dev container's network policy blocks the Pyodide download — it runs in this repo's CI verify workflow.

  • Manual: rendered pages screenshotted via headless Chromium — dark-mode example page, home with search results open, the mutability/positional-only pair banners, and sample social cards.

  • New/modified tests pass

  • Tests fail when the change is reverted (regression guard)

  • Full test suite passes (no regressions)

  • Manual testing completed (described above)

Screenshots / Recordings

Visual changes: dark palette, search box + results dropdown on home, pair banners between cells, and the social cards. To reproduce locally:

  • Pages: uv run --group workers pywrangler dev --port 9696, then visit /, /examples/mutability, /examples/positional-only-parameters (toggle OS dark mode for the palette).
  • Social cards: make social-cards regenerates public/og/*.jpg (set CHROME_PATH if Chrome isn't at the default location); the committed files render at /og/<slug>.jpg.
  • The banner layout was validated against the pre-existing prototypes at /prototyping/layout-banner-{single,pair,trio}.html.

Risk

  • The layout template and _layout signature changed, so every page re-renders — the HTML cache version rolls on deploy (intentional, existing mechanism) and the CDN re-fills.
  • The walkthrough renderer changed from a per-cell loop to _render_walkthrough; pages without banners render byte-identically (covered by the existing rendering-contract tests).
  • 7.2 MB of committed JPEGs in public/og/ — deliberately excluded from make check-generated because rasterized bytes vary across Chrome versions; the SEO linter checks existence instead, so a new example without a card fails verification.
  • editor.js/syntax-highlight.js changed (fingerprints roll); the CodeMirror one-dark import adds one esm.sh module fetched only by the editor page.
  • No example source (src/example_sources/*.md) content changed; golden parity is at 100%.

https://claude.ai/code/session_012tBECRGRPmM2dj5b3FFJGo

claude added 3 commits July 2, 2026 07:36
…lytics

Distribution and product-polish pass:

- /sitemap.xml route + robots.txt Sitemap directive; JSON-LD structured
  data (WebSite on home, TechArticle/LearningResource on examples),
  enforced by the SEO linter.
- Dark mode via prefers-color-scheme: inverted warm palette, dual-theme
  Shiki, dark CodeMirror highlight style, marginalia figures on a light
  paper chip so the locked grammar stays untouched. Skip-to-content
  link on every page.
- Client-side example search on the home page: build-step JSON index,
  fingerprinted search.js/search-index.json assets, "/" shortcut, Node
  ranking check wired into make verify.
- Per-example social cards composed from each example's marginalia
  figure, rasterized with headless Chrome to public/og/<slug>.jpg and
  referenced via og:image/twitter:card (make social-cards).
- Learner-behavior report aggregating exported Worker wide events into
  most-read pages, most-run examples (edited/error shares, execution
  percentiles), journey traffic, and missing-example 404s.
- Journeys now cover all 109 examples: five previously unreferenced
  examples joined their natural sections with outcome-registry support
  updates; stale "gap placeholder" copy removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tBECRGRPmM2dj5b3FFJGo
- render_banner(slug, position) implements the visual-explainer-spec
  grammar: before / after-cell-N (legacy anchor cell-N) /
  after-walkthrough positions, with multiple figures per position
  rendering as one small-multiple banner. ATTACHMENTS stays the single
  registry, so all five contract families keep their coverage.
- _render_walkthrough in app.py interleaves cells with banners at all
  three positions; render_for_anchor remains as a spelling wrapper.
- Seed the canonical mutability pair: aliased mutation beside the
  frozen tuple, matching the layout-banner-pair prototype.
- Anchor contract test accepts the position grammar; new BannerTests
  cover the pair, position resolution, and before/after placement.
- visual-explainer-spec.md updated to present tense; lessons-learned
  gains a discoverability/theming/analytics section and the stale
  inline-layout bullet now describes the shipped banner grammar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tBECRGRPmM2dj5b3FFJGo
Four multi-figure/banner additions, each anchored to the cell whose
lesson is the contrast:

- positional-only-parameters: the / and * separator twins share one
  banner under the signature that contains both markers.
- metaclasses: metaclass-triangle beside class-triangle — the rhyming
  pair reads as "the same triangle one level down".
- tuples: tuple-frozen moves to the "different intent" cell and pairs
  with list-append (fixed shape vs. growing collection).
- iterator-vs-iterable: iterator-unroll lands on the exhaustion cell
  where list(stream) drains the iterator.

Subject figures stay first in each pair, so every social card is
unchanged. Captions are bespoke per slug per the reuse rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tBECRGRPmM2dj5b3FFJGo
@adewale adewale changed the title Add learner analytics, search, social cards, and dark mode Add discoverability, dark mode, search, social cards, banner grammar, and learner analytics Jul 2, 2026
- P2: the FastAPI catch-all wrapped every route() response in
  HTMLResponse, discarding AppResponse.headers — deployed /sitemap.xml
  shipped as text/html. The catch-all now preserves headers when the
  route sets them. Regression tests exercise the handler through the
  stubbed-main harness (extracted MainModuleHarness) and fail when the
  fix is reverted.
- P3: search results were rendered via innerHTML with interpolated
  catalog fields. Result nodes are now built with createElement/
  textContent and replaceChildren, the slug is URI-encoded in the href,
  and the ranking check asserts search.js contains no innerHTML use.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012tBECRGRPmM2dj5b3FFJGo
@adewale adewale merged commit b61edf1 into main Jul 2, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants