Add discoverability, dark mode, search, social cards, banner grammar, and learner analytics#7
Merged
Conversation
…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
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
/sitemap.xmlroute inapp.py,public/robots.txtwith a Sitemap directive, and JSON-LD (WebSiteon home,TechArticle/LearningResourceon examples) injected through a new_layoutparameter.scripts/lint_seo_cache.pynow enforces all of it.prefers-color-scheme: darkblock overriding the existing CSS custom properties. Shiki moves to dual themes (CSS-variable swap); CodeMirror picksoneDarkHighlightStyleviamatchMedia. Marginalia figures render on a light "paper" chip in dark mode, so the locked SVG grammar is untouched.scripts/build_search_index.pyemits a JSON index (title/section/summary/notes text, lowercased at build time) that goes through the existing fingerprint pipeline;public/search.jsis a dependency-free ranked matcher with a/shortcut and keyboard navigation. Chosen over a search library to keep the zero-dependency asset story.scripts/build_social_cards.pycomposes a 1200×630 HTML card per example around its curated marginalia figure;scripts/build_social_cards.mjsrasterizes all 110 through one headless-Chrome CDP session topublic/og/<slug>.jpg(JPEG q90 — 7.2 MB total vs 17 MB PNG). Pages reference them viaog:image/twitter:card.render_banner(slug, position)implementsbefore/after-cell-N(legacy anchorcell-N) /after-walkthrough, with multiple figures per position rendering as one small-multiple banner.ATTACHMENTSstays the single registry — extending its anchor vocabulary (instead of the spec's proposed parallelBANNERSdict) 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.scripts/learner_report.pyconsumes 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 indocs/learner-analytics.md.Testing
26 new tests across
tests/test_app.py(Discoverability, DarkModeAndAccessibility, Search, SocialCard, Banner suites) andtests/test_learner_report.py; the geometry anchor contract now accepts the position grammar.scripts/check_search_ranking.mjs(newmake search-ranking-test, wired intomake 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 andDiscoverabilityTests).Not run locally:
make browser-layout-testneeds 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:
uv run --group workers pywrangler dev --port 9696, then visit/,/examples/mutability,/examples/positional-only-parameters(toggle OS dark mode for the palette).make social-cardsregeneratespublic/og/*.jpg(setCHROME_PATHif Chrome isn't at the default location); the committed files render at/og/<slug>.jpg./prototyping/layout-banner-{single,pair,trio}.html.Risk
_layoutsignature changed, so every page re-renders — the HTML cache version rolls on deploy (intentional, existing mechanism) and the CDN re-fills._render_walkthrough; pages without banners render byte-identically (covered by the existing rendering-contract tests).public/og/— deliberately excluded frommake check-generatedbecause 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.jschanged (fingerprints roll); the CodeMirror one-dark import adds one esm.sh module fetched only by the editor page.src/example_sources/*.md) content changed; golden parity is at 100%.https://claude.ai/code/session_012tBECRGRPmM2dj5b3FFJGo