← Back to home
// platform guide

Create a Course with AI

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:

$ npm install $ npm run new-course -- python $ npm run build

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.

How it works

  1. Scaffold. npm run new-course -- <slug> creates courses/<slug>/ with a minimal course.yaml, a starter lesson, and the directory structure for exercises, flashcards, and assets.
  2. Generate content. Open STARTER_PROMPT.md and paste it into Claude Code, Cursor, Copilot, Augment, or any AI coding tool. It contains full schemas for every content type. The AI generates lessons, exercises, flashcards, and optional extras. See section 2 for tool-specific workflows.
  3. Build. npm run build compiles markdown + YAML into a self-contained static site. Output goes to dist/. Host it anywhere.
Prerequisites
Node.js and npm. Install dependencies once with npm install, then npm run build to compile. No other tooling required.

What the engine provides

You provide content (markdown lessons, YAML exercises). The engine handles everything else:

Project structure

vibe-learn/ engine/ # Platform code — don't modify for content templates/ # HTML templates (landing, guide, lessons, etc.) plugins/ # Feature plugins (flashcards, algorithms, etc.) js/ # Runtime JS (exercises, SRS, themes) css/ # Base styles themes/ # Color theme CSS files courses/ # One directory per course go/ # Example: Go course sample/ # Example: Sample course build.js # Build system — compiles everything to dist/ STARTER_PROMPT.md # AI generation prompt with all schemas CLAUDE.md # Claude Code auto-context

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.

Claude Code

Terminal — runs in your project directory

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.

  1. Open a terminal in the project root
  2. Run claude to start a session
  3. Say: "Create a course about Python for JavaScript developers"
  4. Claude reads CLAUDE.md, follows STARTER_PROMPT.md, scaffolds the course, and generates all content files
  5. Review the output, then npm run build

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

IDE — Composer or inline chat

Cursor's Composer mode works well for multi-file generation. Use the Agent or Composer panel.

  1. Open the project in Cursor
  2. Open Composer (Cmd+I / Ctrl+I)
  3. Paste the contents of STARTER_PROMPT.md into the prompt, or reference it: "Read STARTER_PROMPT.md and create a course about Rust for C++ developers"
  4. Composer generates files across the course directory
  5. Review the generated files in the editor, then npm run build in the terminal
Tip
For large courses (8+ modules), generate one module at a time in separate Composer prompts. This keeps the context window focused and produces higher quality output.

GitHub Copilot

VS Code — Copilot Chat panel

Use Copilot Chat in agent mode (@workspace) to generate course content.

  1. Open the project in VS Code with Copilot enabled
  2. Open Copilot Chat (Ctrl+Shift+I)
  3. Paste STARTER_PROMPT.md contents into the chat, or tell it: "Read STARTER_PROMPT.md and follow its instructions to create a course about Kubernetes"
  4. Review generated files and run npm run build

Windsurf

IDE — Cascade flow

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.

Augment

IDE chat or Auggie (terminal agent)

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.

ChatGPT, Claude.ai, or any LLM

Browser — copy/paste workflow

Any LLM that can output YAML and markdown works. The process is more manual but still effective.

  1. Run npm run new-course -- <slug> to scaffold the directory
  2. Open STARTER_PROMPT.md, fill in the configuration section at the top (topic, slug, number of modules, etc.)
  3. Paste the entire file into your LLM conversation
  4. The LLM generates course.yaml, lesson markdown files, exercise YAML, and flashcards following the schemas
  5. Copy each generated file into the correct location under courses/<slug>/
  6. Run npm run build to compile
Tip
Generate one module at a time (lesson + exercises + flashcards for that module) rather than the entire course at once. This produces better results and avoids hitting context limits.

Getting better results


Each course lives in its own folder under courses/. The build discovers it automatically — no registration step.

Directory structure

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.

courses/my-course/ course.yaml # required — course metadata content/ lessons/ # required — one .md file per module module0.md module1.md module2.md exercises/ # optional — enables exercises + daily practice + analytics module1-variants.yaml module2-variants.yaml flashcards/ # optional — enables flashcard sessions flashcards.yaml algorithms/ # optional — enables algorithm practice algorithms.yaml real-world-challenges/ # optional — enables challenge mode real-world-challenges.yaml assets/ # optional — favicon, images favicon.svg

course.yaml

This is the only configuration file. It defines the course name, module list, and how modules are grouped into tracks.

course: name: "My Course" slug: my-course # becomes the URL path: dist/my-course/ description: "Short description for the landing page." storagePrefix: my-course # localStorage namespace (keep unique per course) tracks: - title: Getting Started modules: [0, 1] - title: Core Concepts modules: [2, 3, 4] modules: - title: Quick Reference # id: 0 (auto-derived from array index) description: "Installation and setup." hasExercises: false # no exercise file for this module - title: Hello World # id: 1, file: module1 description: "Your first program." - title: Variables & Types # id: 2, file: module2 description: "Data types and declarations." - title: Control Flow # id: 3, file: module3 description: "Conditionals and loops." - title: Functions # id: 4, file: module4 description: "Function syntax and closures." projects: # optional — capstone projects - id: p1 num: P1 title: CLI Tool file: project-cli # reads content/lessons/project-cli.md afterModule: 4 # placed after module 4 in sidebar description: "Build a command-line tool."

Auto-derived fields

You only need to write title and description for each module. These fields are derived automatically:

FieldDerived fromExample
idArray index (0-based)0, 1, 2
numZero-padded id00, 01, 02
filemodule + idmodule0, module1
hasExercisesDefaults to trueSet to false for reference-only modules

Draft courses

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.

course: name: "Work in Progress" slug: wip hidden: true # builds, but not listed on landing page

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.

Syntax-highlighted code

Use fenced code blocks with a language tag. The engine uses highlight.js (190+ languages supported).

```go func main() { fmt.Println("hello") } ```

Side-by-side comparisons

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.

*Python* ```python for i in range(5): print(i) ``` *Go* ```go for i := 0; i < 5; i++ { fmt.Println(i) } ```
Note
The two labeled blocks must be consecutive — no blank paragraph or heading between the closing ``` and the next *Label*. A single labeled block (no neighbor) renders with a header but no side-by-side layout.

Callout blocks

Use a blockquote with a bold label. Common labels: Tip, Warning, Note, Important.

> **Tip:** Remember to save your work before clearing browser data. > **Warning:** This operation will delete all stored progress.

Exercise section (critical)

For every module that has exercises (hasExercises: true or omitted), the markdown file must end with this structure:

## Exercises ### Warmups <div id="warmups-container"> <noscript><p class="js-required">JavaScript is required</p></noscript> </div> ### Challenges <div id="challenges-container"> <noscript><p class="js-required">JavaScript is required</p></noscript> </div>
Warning
If these divs are missing, exercises will not render even if the YAML file exists. The build system injects exercise JavaScript that targets these container IDs.

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.

FeatureContent fileAuto-activates when
Exercisescontent/exercises/module<id>-variants.yamlFile exists for any module
Flashcardscontent/flashcards/flashcards.yamlFile exists
Algorithm Practicecontent/algorithms/algorithms.yamlFile exists
Real-World Challengescontent/real-world-challenges/real-world-challenges.yamlFile exists
Daily Practicecontent/exercises/ directory exists
Analyticscontent/exercises/ directory exists

Exercises

Core
content/exercises/module<id>-variants.yaml

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.

conceptLinks: Hash Maps: "#lesson-hash-maps" # link concept names to lesson anchors variants: warmups: - id: warmup_1 concept: Hash Maps # groups exercise under this concept for SRS + analytics variants: - id: v1 title: Simple Lookup description: "Implement a function that looks up a key..." hints: - "Use a dictionary/map for O(1) lookup." - "Handle the case where the key doesn't exist." solution: |- def lookup(data, key): return data.get(key) annotations: - type: tip label: Maps text: ".get() returns None instead of raising KeyError." - id: v2 title: Count Occurrences description: "Count how many times each character appears..." hints: - "Iterate through the string and increment a counter." solution: |- from collections import Counter Counter(text) challenges: - id: challenge_1 concept: Hash Maps variants: - id: v1 title: Group Anagrams description: "Group words that are anagrams of each other..." hints: - "Sort each word's letters to create a grouping key." solution: |- from collections import defaultdict def group(words): d = defaultdict(list) for w in words: d[tuple(sorted(w))].append(w) return list(d.values()) advanced: # optional third tier - id: advanced_1 concept: Hash Maps variants: - id: v1 title: LRU Cache description: "Implement a least-recently-used cache..." hints: [...] solution: "..."
Key features
  • Multiple variants per exercise — repeated practice stays fresh
  • 5 difficulty modes (concept-first through blind)
  • Concept filters to focus on specific topics
  • Thinking timer that locks hints while you work
  • Progressive hints (reveal one at a time)
  • Annotated solutions with contextual tips
  • Self-rating (Got it / Struggled / Needed solution) feeds SRS

Flashcards

Plugin
content/flashcards/flashcards.yaml

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).

"1": # module ID as string - topic: Variables q: What is the zero value of an int in Go? a: "0 — all numeric types zero-value to 0." - topic: Strings q: Are Go strings mutable? a: "No. Strings are immutable byte slices. Use []byte for mutation." "2": - topic: Control Flow q: Does Go have a while loop? a: "No. Use `for` with just a condition: `for x < 10 { }`."
Key features
  • Flip/rate card sessions with configurable deck size
  • Three review modes: random, due (SRS), weakest (lowest ease)
  • Keyboard shortcuts for flipping and rating
  • Filter by module
  • Self-rating integrates with spaced repetition

Algorithm Practice

Plugin
content/algorithms/algorithms.yaml

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.

categories: - id: arrays-searching name: Arrays & Searching icon: "#" order: 1 description: "Hash maps, linear search, binary search" problems: - id: two-sum name: Two Sum concept: "Hash Map Complement" # tracked in SRS + analytics difficulty: 1 # 1-3 scale for sorting/display patternPrimer: # optional — shown before attempting bruteForce: "Nested loops checking all pairs — O(n^2)" bestApproach: "Single pass with hash map storing complements — O(n)" variants: - id: v1 title: Two Sum difficulty: 1 description: "Given an array and a target, return indices that sum to target." hints: - "For each number, its complement is target - num." - "Use a map to store each number's index as you go." solution: | def two_sum(nums, target): seen = {} for i, num in enumerate(nums): if target - num in seen: return [seen[target - num], i] seen[num] = i testCases: | # optional — shown below solution print(two_sum([2, 7, 11, 15], 9)) # [0, 1] print(two_sum([3, 2, 4], 6)) # [1, 2]
Key features
  • Session-based drills with category and count selection
  • Mastery bars showing per-problem SRS progress
  • Blind mode (no description, just the problem title)
  • Pattern recognition primers (brute-force vs. optimal)
  • Review / discover / weakest / mixed modes via SRS

Real-World Challenges

Plugin
content/real-world-challenges/real-world-challenges.yaml

Open-ended engineering problems with requirements, acceptance criteria, progressive hints, and optional extensions. Each challenge is tagged by company and concept.

challenges: - id: todo-api title: "Todo REST API" difficulty: 2 # 1-3 dots shown in UI companies: [Acme Corp, Contoso] # shown as tags concepts: [REST, JSON, CRUD] # shown as tags source: "Classic backend project" # attribution sourceUrl: "https://example.com" # optional link requirements: | Build a REST API for managing a todo list. Your API should support: - GET /todos — list all todos - POST /todos — create a new todo - PUT /todos/:id — update a todo - DELETE /todos/:id — delete a todo Each todo has an id, title, completed flag, and createdAt timestamp. Store todos in memory (no database required). acceptanceCriteria: - "GET /todos returns a JSON array of all todos" - "POST /todos creates a todo and returns 201" - "GET /todos/:id returns 404 for missing IDs" - "DELETE /todos/:id returns 204" hints: - title: "Getting started" # progressive — revealed one at a time content: | Pick any HTTP framework. Use an in-memory slice or map to store todos. - title: "Status codes" content: | 201 Created for POST, 204 No Content for DELETE, 404 Not Found for missing resources. extensions: # optional stretch goals - "Add pagination to the list endpoint" - "Add filtering by completion status"
Key features
  • Interactive acceptance criteria checklist
  • Status tracking (not started / in progress / completed)
  • Difficulty dots (1–3)
  • Progressive hints (revealed one at a time)
  • Company and concept tags for filtering

Daily Practice

Plugin
Auto-activates when content/exercises/ exists — no content file needed

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).

Key features
  • 4 modes: review (due items), discover (unseen), weakest (lowest ease), mixed
  • Auto-selects the best mode based on how many items are due vs. weak
  • Module filter to focus on specific modules
  • Configurable session size

Analytics

Plugin
Auto-activates when content/exercises/ exists — no content file needed

Grafana-style dashboard showing your learning health at a glance. Reads SRS data from localStorage — no additional content files required.

Key features
  • Health cards: mastery %, due count, streak, 30-day sparkline trend
  • Module health grid with color-coded cells and hover tooltips
  • Auto-generated action items (due reviews, weakest concepts, decaying modules)
  • Collapsible detail panels for modules, concepts, weakest exercises, and ratings

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.

Self-rating

After completing an exercise or flashcard, you rate yourself on a three-point scale:

RatingEffect on scheduling
Got itInterval increases — the item comes back later
StruggledInterval stays short — the item comes back soon
Needed solutionInterval 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.

Where SRS is used

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).

ThemeDescription
DarkDefault. Uses CSS variables defined in :root in style.css.
LightLoaded 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.

Plugin structure

A plugin is a directory in engine/plugins/<name>/ containing:

FilePurpose
manifest.jsonMetadata: name, sidebar label/color/order, content pattern, scripts, data file config
HTML templateThe page template (can include partials via {{> name}})
JS file(s)Runtime logic, copied to dist alongside the page
build.jsOptional — 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.

{ "name": "my-feature", "label": "My Feature", // sidebar + nav pill label "shortLabel": "MF", // short label for compact UI "sidebarColor": "var(--cyan)", // accent color in sidebar "sidebarOrder": 6, // position in sidebar (lower = higher) "template": "my-feature.html", // HTML template in this directory "scripts": ["my-feature.js"], // JS files to copy to dist "dataFile": "my-feature-data.js", // generated JS data file name "dataGlobal": "MyFeatureData", // window.MyFeatureData global "contentPattern": "my-feature/data.yaml", // activates when this file exists "backupKey": "my-feature", // localStorage key for data export "prefix": "mf", // element ID prefix for partials "nextFn": "MyFeature.next()", // onclick for session-nav Next button "skipFn": "MyFeature.skip()" // onclick for session-nav Skip button }

Template partials

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.

PartialWhat 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:

Session engine (session-engine.js)

Shared runtime library for session-based plugins. Available globally as window.SessionEngine. Provides session lifecycle management, SRS queue building, and DOM utilities.

FunctionDescription
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)

Shared CSS classes

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.

ClassWhat it styles
session-configConfiguration panel (selectors shown before a session starts)
session-config-row / session-config-label / session-config-optionsRow layout within config panels: label on the left, option buttons on the right
session-optionToggle button in option groups — handles .active state, hover, accent color
session-stat / session-stat-value / session-stat-labelStat counters displayed in the config panel (e.g. "12 due", "5 weak")
session-header / session-header-progressHeader bar shown during a session with current position
session-progress-bar / session-progress-fillAnimated progress bar (width set via JS)
session-complete / session-complete-statsResults screen shown when a session ends
session-start-btn-mainPrimary action button (Start Session, New Session)
focus-contentContent 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:

[data-module="my-feature"] { --session-accent: var(--cyan); }

Exercise core (exercise-core.js)

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.


Building

npm run build # build all courses + landing page node build.js my-course # build a single course (by slug)

Output goes to dist/. Each course gets its own subdirectory (dist/my-course/) and the landing page is generated at dist/index.html.

Deploying

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.