Scaffold a course, tell your AI tool what to teach, run one build command. You get a static site with lessons, exercises, flashcards, spaced repetition, and analytics — no backend, no database.
Most courses teach a language in the abstract. You learn syntax, do toy exercises, and never build anything real. vibe-learn exists because learning works better when the course is built around your goal — learning Go to build CLI tools, learning Rust to write a game engine, learning Python to automate your data pipeline.
With AI tools, you can generate a course tailored to exactly what you want to build, with exercises and examples that use your target domain. Three commands:
The scaffold command creates a course directory with the right file structure. From there, you either write content manually, use an AI tool to generate it, The build compiles everything into static HTML you can host anywhere.
You provide content (markdown lessons, YAML exercises). The engine handles everything else:
STARTER_PROMPT.md is a self-contained generation prompt that works with any AI coding tool. It includes every schema, every file naming convention, and a verification checklist. Pick a tool below and follow the workflow.
The fastest path. Claude Code reads the CLAUDE.md file automatically, which points it to STARTER_PROMPT.md. You don't need to paste anything.
Claude Code can also iterate on existing courses: "Add 3 more modules to the Python course about decorators, generators, and async" — it reads the existing course.yaml, understands the structure, and extends it.
Cursor's Composer mode works well for multi-file generation. Use the Agent or Composer panel.
Use Copilot Chat in agent mode (@workspace) to generate course content.
Windsurf's Cascade mode handles multi-file generation. Open the project, start a Cascade flow, and paste STARTER_PROMPT.md with your topic. Cascade creates the files and you can review diffs before accepting.
Use the Augment chat panel in your IDE, or run auggie in the terminal for an agentic workflow similar to Claude Code. Either way, reference STARTER_PROMPT.md and describe your course topic. Augment generates the files across the course directory. Review the output and run npm run build.
Any LLM that can output YAML and markdown works. The process is more manual but still effective.
Each course lives in its own folder under courses/. The build discovers it automatically — no registration step.
The scaffold command (npm run new-course -- <slug>) creates this for you. Only course.yaml and content/lessons/ are required. Every other subdirectory is optional — add them when you want to enable the corresponding feature.
This is the only configuration file. It defines the course name, module list, and how modules are grouped into tracks.
You only need to write title and description for each module. These fields are derived automatically:
| Field | Derived from | Example |
|---|---|---|
id | Array index (0-based) | 0, 1, 2 |
num | Zero-padded id | 00, 01, 02 |
file | module + id | module0, module1 |
hasExercises | Defaults to true | Set to false for reference-only modules |
Add hidden: true to the course block to exclude a course from the landing page. It still builds and is accessible by URL — it just won't appear in the course picker.
Lessons are standard Markdown files in content/lessons/, one per module, named to match the module's file field: module0.md, module1.md, etc. The build converts them to HTML with syntax highlighting and a few extra features.
Use fenced code blocks with a language tag. The engine uses highlight.js (190+ languages supported).
Put an italic label (*Label*) on its own line before a fenced code block. When two or more labeled blocks appear consecutively, the build wraps them in a side-by-side layout automatically.
Use a blockquote with a bold label. Common labels: Tip, Warning, Note, Important.
For every module that has exercises (hasExercises: true or omitted), the markdown file must end with this structure:
Beyond lessons, a course can include exercises, flashcards, algorithm problems, and real-world challenges. Each content type is either a core feature (exercises) or a plugin that auto-activates when the matching content file is present. Two plugins (daily practice, analytics) derive from exercise data and require no content files of their own.
The table below summarizes what to add and where. Detailed YAML format for each type follows.
| Feature | Content file | Auto-activates when |
|---|---|---|
| Exercises | content/exercises/module<id>-variants.yaml | File exists for any module |
| Flashcards | content/flashcards/flashcards.yaml | File exists |
| Algorithm Practice | content/algorithms/algorithms.yaml | File exists |
| Real-World Challenges | content/real-world-challenges/real-world-challenges.yaml | File exists |
| Daily Practice | — | content/exercises/ directory exists |
| Analytics | — | content/exercises/ directory exists |
Interactive exercises with multiple variants per problem, progressive hints, annotated solutions, and self-rating that feeds the spaced repetition scheduler (see section 6).
Each module that has hasExercises: true (the default) expects a corresponding file named module<id>-variants.yaml. The file defines warmups, challenges, and optionally advanced exercises. Each exercise contains multiple variants — different problem phrasings testing the same concept. The engine picks one variant at random each time.
Flip-and-rate flashcard sessions organized by module. Cards support markdown in answers.
The file is a YAML map where each key is a module ID (as a string) and the value is an array of cards. Each card has a topic, q (question), and a (answer).
Session-based algorithm drills organized by category, with progressive difficulty variants and optional pattern recognition primers.
The file defines categories (e.g. "Arrays & Searching"), each containing problems with multiple variants. Each problem can include a patternPrimer that explains the brute-force vs. optimal approach before the student attempts the problem.
Open-ended engineering problems with requirements, acceptance criteria, progressive hints, and optional extensions. Each challenge is tagged by company and concept.
Session-based practice that auto-selects exercises based on your SRS data. You don't write any additional content for this plugin — it pulls from your existing exercise data and the spaced repetition scheduler (see section 6).
Grafana-style dashboard showing your learning health at a glance. Reads SRS data from localStorage — no additional content files required.
The platform uses the SM-2 algorithm (the same algorithm behind Anki) to schedule reviews. Every exercise variant and flashcard is tracked independently with its own ease factor and review interval.
After completing an exercise or flashcard, you rate yourself on a three-point scale:
| Rating | Effect on scheduling |
|---|---|
| Got it | Interval increases — the item comes back later |
| Struggled | Interval stays short — the item comes back soon |
| Needed solution | Interval resets to 1 day — the item comes back tomorrow |
Each rating adjusts the item's ease factor. Items you struggle with get a lower ease factor, which means shorter intervals between reviews. The minimum ease factor is 1.3 to prevent impossibly short intervals.
All SRS data is stored in localStorage under each course's storagePrefix. You can export and import progress from the dashboard's data backup panel.
Two themes — Dark and Light — switchable via the sun/moon toggle in the top-right corner. The selection persists in localStorage and defaults to your system preference (prefers-color-scheme).
| Theme | Description |
|---|---|
| Dark | Default. Uses CSS variables defined in :root in style.css. |
| Light | Loaded from themes/light.css. Overrides variables on [data-theme="light"]. |
Themes work by overriding CSS variables on a [data-theme] selector on <html>. Every page — lessons, exercises, plugin pages — automatically inherits the user's chosen theme. No per-page or per-course configuration is needed.
If a user had previously selected a legacy theme (Factorio, Gruvbox, Solarized, Everforest, Terminal), it is automatically migrated to Dark or Light on the next page load.
Plugins add new feature pages to courses. If you want to extend the platform beyond the built-in content types, this section explains how. If you're only writing course content, you can skip this.
A plugin is a directory in engine/plugins/<name>/ containing:
| File | Purpose |
|---|---|
manifest.json | Metadata: name, sidebar label/color/order, content pattern, scripts, data file config |
| HTML template | The page template (can include partials via {{> name}}) |
| JS file(s) | Runtime logic, copied to dist alongside the page |
build.js | Optional — a transform(data, ctx) export for build-time data processing |
The key field is contentPattern. It's a relative path under a course's content/ directory. If a course has a matching file or directory, the plugin activates for that course. No registration, no config to update.
HTML fragments in engine/partials/, included in plugin templates with {{> partial-name}} syntax. The build replaces the include directive with the partial's content, substituting per-plugin placeholders.
| Partial | What it provides |
|---|---|
head |
Full <head> boilerplate: <meta> tags, font loading, favicon, style.css, theme CSS init |
dashboard-nav |
Top navigation with dashboard back-link |
rating-guide |
Expandable "How should I rate?" panel — explains Got It / Struggled / Needed Solution |
session-nav |
Next / Skip button pair for advancing through session items |
session-complete |
"Session Complete" card with results stats and New Session / Dashboard buttons |
Placeholders in partials:
Shared runtime library for session-based plugins. Available globally as window.SessionEngine. Provides session lifecycle management, SRS queue building, and DOM utilities.
| Function | Description |
|---|---|
setupOptionGroup(containerId, btnClass, config, key, opts) |
Wire a group of toggle buttons: manages .active state, updates config[key], fires optional onChange callback |
setActiveOption(containerId, btnClass, btn) |
Programmatically set the active button in a group (useful for dynamically built buttons) |
buildSRSQueue(mode, count, filterFn) |
Build a practice queue from SRS data. Modes: review (due items), weakest (lowest ease), mixed (due + weak) |
buildPaddedSRSQueue(mode, count, filterFn, allItemsFn) |
Like buildSRSQueue but pads short queues with random items to meet the requested count |
buildDiscoverQueue(allItems, count) |
Build a discovery queue: unseen items first, then seen items, shuffled within each group |
updateStats(elementMap, filterFn) |
Query SRS data and write due/weak/total counts to DOM elements |
escapeHtml(str) |
Escape &, <, >, ", ' for safe HTML rendering |
setText(id, text) |
Set textContent of element by ID |
show(id) / hide(id) |
Toggle element visibility by ID (removes/adds hidden attribute) |
The main stylesheet provides unified classes for session-based plugin UI. Using these means your plugin gets consistent styling across all themes with no per-plugin CSS.
| Class | What it styles |
|---|---|
session-config | Configuration panel (selectors shown before a session starts) |
session-config-row / session-config-label / session-config-options | Row layout within config panels: label on the left, option buttons on the right |
session-option | Toggle button in option groups — handles .active state, hover, accent color |
session-stat / session-stat-value / session-stat-label | Stat counters displayed in the config panel (e.g. "12 due", "5 weak") |
session-header / session-header-progress | Header bar shown during a session with current position |
session-progress-bar / session-progress-fill | Animated progress bar (width set via JS) |
session-complete / session-complete-stats | Results screen shown when a session ends |
session-start-btn-main | Primary action button (Start Session, New Session) |
focus-content | Content area styling used by focus mode on plugin pages |
To customize the accent color for your plugin, override the CSS variable on the page's data attribute:
Shared exercise rendering utilities available as window.ExerciseCore. Provides difficulty mode selection, variant picking, thinking timer, and shuffle logic. Used by both module lesson pages and the algorithms plugin. If your plugin renders exercises in the same format, you can reuse this instead of writing your own renderer.
Output goes to dist/. Each course gets its own subdirectory (dist/my-course/) and the landing page is generated at dist/index.html.
The output is pure static files — no server runtime, no database, no build step at deploy time. Host the dist/ directory anywhere that serves static files:
All user progress is stored in the browser's localStorage. There is no backend to set up, no database to manage, and no user accounts. Each course is isolated via its storagePrefix.