Files
astrolabe/docs/architecture/11-learning-section.md
T

5.2 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.htmlsrc/learn/) alongside the landing (/) and the app (/app/), under the same marketing-surface rules:

  • Reuses src/core and the landing's LandingChart only — never stores, modals, orchestration, or app components. Vega is lazy-loaded through LandingChart, 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 by scripts/learn-pages.ts (run from vite.config on every dev/build, so "drop a file" still holds; the shells are git-ignored). The single src/learn entry renders the index or one lesson from location.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. Every lesson uses the :::data form (never per-stage inline rows), targets a misconception rather than a chart type, and closes with a "take it further" prose beat that leans on the per-stage "Open in Astrolabe" links — the roster and per-lesson briefs live in docs/exploration/lessons-roadmap.md.

The pipeline

lessons/*.mdimport.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, and formatSpec take parsed blocks and specs and know nothing about markdown — the parser is the only thing coupled to the .md format. 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 in src/learn. Core stays dependency-freemarked is imported only by src/learn/Markdown.
  • dangerouslySetInnerHTML renders 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 with formatSpec so the shown JSON matches the editor's house style.
  • Lesson charts render through LandingChart with fitMode: 'width' — which sets width: "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 a vconcat (a stacked column), not an hconcat dashboard, which would fight the sizing.