A static web front end for the courses under courses/. It reads the pipeline's output and
renders it; it never writes to it.
There is one source of truth, and it is the YAML and Markdown the agents produce. This app holds no course content of its own — no database, no CMS, no duplicated copy of a lesson. Regenerate a lesson and rebuild, and the site says what the lesson now says.
cd web
npm install
npm run devThen open the URL it prints. Astro 7 runs the dev server as a daemon, so the command returns immediately:
npx astro dev status
npx astro dev logs
npx astro dev stop| Script | Does |
|---|---|
npm run dev |
dev server with live reload on any change under courses/ |
npm run build |
type-checks, then writes the static site to web/dist/ |
npm run preview |
serves web/dist/ as it will be served in production |
npm run check |
astro check on its own |
Adding a course is dropping a directory into courses/. Nothing registers it, because
nothing has a list to register it in: the route generators enumerate the directory at build
time, so a course exists on the site exactly when it exists on disk.
In dev that enumeration has to be redone when the disk changes, and Astro caches
getStaticPaths() results per route. A Vite plugin (src/lib/watch-courses.ts)
watches courses/ and signals astro:content-changed, which is what clears that cache; a
plain browser reload would not, and the page would come back with the same stale routes. Add
a course, edit a lesson, delete a glossary — the open tab reflects it without a restart.
In production it is a plain static build. Rebuild to publish.
Learner-facing artefacts only:
| File | Rendered as |
|---|---|
course.yaml |
the course's module and lesson structure |
audience.yaml |
who the course is for, prerequisites, misconceptions |
curriculum.yaml |
module rationale, pacing, cognitive load, skills unlocked |
outcomes.yaml |
per-lesson outcomes with Bloom level and evidence |
<lesson>.md |
the lesson page |
<lesson>.exercises.yaml |
practice, with hints revealed one at a time |
<lesson>.quiz.yaml |
a self-marking knowledge check |
glossary.yaml |
an alphabetised glossary linking each term to the lesson it appears in |
project.yaml |
the capstone brief, milestones and rubric |
assessment.yaml and the *.plan.yaml files are deliberately not rendered. They are the
instructions the pipeline gave itself, and showing a learner the answer key and the lesson's
own blueprint would undercut the artefacts that depend on them.
Quiz prompts, exercise tasks and solutions carry fenced code blocks and inline code — a question about a diff is the diff — so those fields are rendered as Markdown rather than as text. They go through the same processor and the same Shiki themes as lesson Markdown, so a code block in a quiz looks like the one in the lesson above it.
They are also rendered with the same trust: raw HTML in a YAML string would reach the page,
exactly as it would in a lesson .md. Both come from the repository.
A lesson writes a diagram as a ```mermaid block, and that block is the only copy. The
fence is lifted out before Markdown rendering — Shiki would otherwise highlight it as source
code — and put back as a pre.mermaid, which
MermaidDiagrams.astro draws once the page loads.
Nothing is pre-rendered to an image, so a regenerated lesson cannot leave a stale picture
behind, and a page with no diagram on it never loads the library.
The diagram is themed from the site's own CSS tokens rather than a packaged Mermaid theme, so it follows light and dark with everything else. Before the script runs, and if it never does, the block shows its own source.
The prose column is narrow, so each drawn diagram gets an Expand button that reopens it
against the whole viewport in a <dialog>. It is labelled from the diagram's own accTitle,
and closes on Escape, on the backdrop, or on the close button.
This is a static site, so correct answers, distractor explanations, hints and exercise solutions all ship to the browser. Anyone reading the HTML can see them. That is the accepted trade for having no server; it is fine for self-study and unfit for anything graded.
Lesson completion and quiz scores are kept in localStorage under st-course-progress/v1.
Per browser, never sent anywhere, and clearable per course from the course page. Every read
and write is guarded, so the site still works with storage disabled — it just stops
remembering.
src/
assets/logo.png site mark, resized and served as webp by astro:assets
lib/
courses.ts the only module that touches disk; never throws, collects warnings
schema.ts lenient zod schemas mirroring the pipeline's YAML
markdown.ts renders the Markdown held in YAML prose fields
shiki.ts highlighting config, shared with astro.config.ts
paths.ts courses/ location, injected at build time
watch-courses.ts dev-only watcher
format.ts
content.config.ts lesson Markdown as an Astro content collection
components/ quiz, exercises, progress, badges, mermaid diagrams
layouts/BaseLayout.astro
pages/
index.astro the library
courses/[course]/index.astro course overview
courses/[course]/[module]/[lesson].astro lesson
courses/[course]/glossary.astro
courses/[course]/project.astro
404.astro
scripts/progress.ts localStorage
styles/global.css
astro.config.ts
courses.ts is deliberately the only file that reads the filesystem, and it is deliberately
forgiving: a course with a malformed project.yaml loses its project page rather than
breaking the build. Malformed artefacts are recorded as warnings, and the pipeline's own
validator is what should be catching them.
Paths inside a course are resolved against the course directory and rejected if they escape
it, so a path: in a manifest cannot be used to read arbitrary files.
- The site is served from the domain root. To host it under a subpath, set
basein astro.config.ts. courses/sits outside the project root, which is whyvite.server.fs.allowincludes... That is a dev-server setting and has no effect on the built output.- Code blocks are highlighted by Shiki with a light and a dark theme, switched by
prefers-color-scheme. There is no theme toggle.