5.0 KiB
11 — The Learning Section (/learn/)
An interactive deep-dive into Vega-Lite, served at /learn/: a marketing surface separate
from the app, where each lesson walks a spec from an 80%-naive version to a polished one to
teach the grammar's dormant power and funnel readers into the app. It is pitched past the
basics — fundamentals are left to the official Vega-Lite docs (linked from the index), so
the section leads with advanced cases (interaction, composition, transforms) rather than
fundamentals.
A marketing surface, like the landing
/learn/ is a third Vite entry (learn/index.html → src/learn/) alongside the landing
(/) and the app (/app/), under the same marketing-surface rules:
- Reuses
src/coreand the landing'sLandingChartonly — never stores, modals, orchestration, or app components. Vega is lazy-loaded throughLandingChart, so the entry stays light. - Out of PWA scope. The service worker is scoped to
/app/, so the learning pages stay uncontrolled, always-fresh, and indexable — the point for organic-reach content. - An index plus a page per lesson.
/learn/is the index (a card per lesson); each lesson is its own URL/learn/<slug>/— a separate indexable document with its frontmatter-derived<title>/<meta>. The per-lesson HTML shells are generated from the lesson frontmatter byscripts/learn-pages.ts(run fromvite.configon every dev/build, so "drop a file" still holds; the shells are git-ignored). The singlesrc/learnentry renders the index or one lesson fromlocation.pathname.
Lessons are markdown; the renderer is general
A lesson is a .md file in src/learn/lessons/, discovered with import.meta.glob — so
adding a lesson is dropping a file, with no registry to edit. A lesson is a free-form
document of ordered blocks, not a fixed template:
| Authored as | Block | Rendered by |
|---|---|---|
| plain markdown | prose |
Markdown |
:::progression wrapping ## stages + fenced specs |
progression |
SpecProgression |
a bare fenced vega-lite block |
chart |
LandingChart |
:::name … ::: |
callout |
Markdown (styled by name) |
:::data wrapping a { name: rows } JSON object |
— metadata | injected into specs at render |
Inside a :::progression, each ## heading is a stage: heading → tab label, prose → note,
the following fenced vega-lite block → spec. A :::data block names datasets once for the
whole lesson; specs reference them with { "data": { "name": … } } and injectDatasets
merges the rows in at render time — so a shared dataset isn't repeated per stage, and the
source pane keeps a stage's grammar legible instead of burying it under data.
The pipeline
lessons/*.md → import.meta.glob + parseLesson (src/learn/lessons.ts, using
core/lesson-parse) → LESSONS. The src/learn entry reads the path: /learn/ →
LearnIndex, /learn/<slug>/ → LessonView, both inside LearnLayout (shared
header/footer/theme). LessonView dispatches each block → Markdown | SpecProgression |
LandingChart. SpecProgression renders each stage's spec with formatSpec
(core/json-format) and highlights what the stage changed with changedLines
(core/spec-diff, an LCS line-diff). parseLesson also returns datasets (the :::data
blocks); LessonView/SpecProgression call injectDatasets so only the rendered spec
carries the rows — the displayed-and-diffed spec keeps its by-name reference.
Rules
- The engine consumes plain data, so the authoring surface is swappable.
SpecProgression,spec-diff, andformatSpectake parsed blocks and specs and know nothing about markdown — the parser is the only thing coupled to the.mdformat. The authoring format can change without touching the widget. - Parsing, diff, and formatting are pure and live in
core(tested hardest); rendering and the markdown library live insrc/learn. Core stays dependency-free —markedis imported only bysrc/learn/Markdown. dangerouslySetInnerHTMLrenders first-party lesson files (repo content authored by us), never user input — not an XSS surface.- Lesson specs are fenced JSON parsed with
JSON.parse; the source pane re-formats them withformatSpecso the shown JSON matches the editor's house style. - Lesson charts render through
LandingChartwithfitMode: 'width'— which setswidth: "container"and drops fixed heights on every view, and container width only works for a single or layered view, not side-by-side. A multi-view lesson is therefore avconcat(a stacked column), not anhconcatdashboard, which would fight the sizing.