Courses, Teachers & Schools
A file-based learning platform — no database. Courses, teachers, and schools are
Markdown pages (create/edit/delete them with the normal docs editor); lessons are
Markdown pages too; enrollments and reviews are JSON on disk. Payments reuse the
shop's products/_payments.md.
Image paths resolve against the asset base (public/).
Courses
Courses index
courses.md with courses_dir: courses renders the course grid.
A course page
One file per course in courses/. Body = the Outline tab intro.
| Key | Description | ||
|---|---|---|---|
title | Course name. | ||
level | Beginner / Intermediate / Advanced — shown as a badge. | ||
price | Number. 0/omitted = free (enroll is instant, no payment). | ||
currency | Symbol. Default $. | ||
image | Hero + card image. | ||
summary | Lead line (alias description); also the card blurb. | ||
school | Owning school's file basename (links course to school). | ||
teacher | Teacher's file basename (links to the profile). | ||
teacher_bio | Short bio shown in the Teacher tab. | ||
start_date | Shown in the right panel. | ||
end_date | Shown in the right panel. | ||
objectives | Label list -> the Objectives tab ("what you'll learn"). | ||
outline | `Title\ | lesson-ref\ | note list -> the numbered, linkable **Outline** (alias lessons`). See Lessons. |
payment_methods | Allowed method ids. Empty = all configured. | ||
related | Course basenames to feature. | ||
course | true to force course rendering outside courses/. |
---
title: Intro to Web Development
level: Beginner
price: 40
currency: $
school: sepodesk-academy
teacher: jane-okoro
teacher_bio: Jane has taught web dev for 8 years.
start_date: 2026-09-01
end_date: 2026-11-30
objectives: [Write semantic HTML, Style with CSS, Deploy a site]
outline:
[
HTML foundations|html-foundations|Structure,
CSS layout|css-layout|Flexbox & grid,
]
payment_methods: [bank, paypal]
related: [advanced-css]
---
The course page has tabs (Outline / Objectives / Teacher / Reviews), star reviews, related courses, and a right panel showing level, enrolled count, start/end dates, price, and an Enroll / Enroll & pay button.
Lessons
Each course keeps its lessons in a folder of the same name next to the course file. The course file holds the meta; the folder holds the content:
content/pages/courses/intro-web-dev.md <- the course (meta, outline)
content/pages/courses/intro-web-dev/ <- its lessons folder
html-foundations.md
css-layout.md
js-basics.md
A lesson page is an ordinary Markdown file — give it a title and an order:
---
title: HTML foundations
summary: Structure and semantics
order: 1
---
# HTML foundations
Your lesson content...
Adding a lesson = dropping a new .md file into the course's folder. There
is no separate registration step.
Linking lessons from the outline
Each outline entry is Lesson title | lesson-ref | note, where lesson-ref is
the lesson file's basename inside the course folder (no .md, no path):
outline:
[
HTML foundations|html-foundations|Structure and semantics,
CSS layout|css-layout|Flexbox and grid,
JavaScript basics|js-basics|Variables to the DOM,
]
- Provide
lesson-refand the outline item becomes a link to that lesson. - Omit
lesson-ref(justTitleorTitle|note) and it renders as plain, unlinked text — useful for section headers. - Omit
outlineentirely and the course auto-lists every lesson in its folder, ordered by each lesson'sorderfront matter.
Locking lessons
Lesson access is gated by enrollment and payment. The rule:
| Course | A viewer may open a lesson when... |
|---|---|
| Free | they have an active enrollment. |
| Paid | they have a paid enrollment. |
| Any | they are an admin (canEdit) — always, for authoring. |
A pending enrollment (a manual bank/cash payment not yet confirmed) stays locked until an admin marks it paid.
This is enforced in two places:
- On the outline (cosmetic): unlocked lessons appear as links; locked ones are greyed with a lock icon and aren't clickable.
- On the lesson page (the real gate): the page checks the parent course's price and the viewer's enrollment status server-side and refuses to render the lesson body unless access is granted. A locked visitor sees a clear message (log in / enroll / complete payment / payment pending) and a button back to the course. Because the gate lives on the page, it holds even if a lesson URL is shared or guessed.
Unlocked lessons render with a breadcrumb back to the course and prev/next navigation across the folder.
The lock controls page rendering. If a lesson embeds a downloadable file or video by a direct
public/URL, that asset is not itself gated — stream protected media through an auth-checked route (like the shop's/shop/download) instead of linking it directly.
Enrollment
Clicking Enroll opens a form collecting the student's full name, email, national ID, gender, location, and date of birth, plus a payment method for paid courses. On submit:
- Free course -> enrolled with status active.
- Paid,
instantmethod -> paid. Paid,manual/link-> pending with instructions until an admin marks it paid.
One enrollment per user per course. The email captured here is how you collect student emails.
The enrollment record holds personal data (national ID, DOB, gender). Keep
content/lms/outside the public web root.
My Courses
my_courses: true -> the signed-in student's own enrollments (course, level,
amount, status).
Enrolled Students (admin)
enrolled_students: true (admin-only) -> every enrollment across all courses,
with Activate / Mark paid / Remove, CSV + PDF export, and 10-per-page
pagination. This view exposes student PII — admins only.
Teachers
Index / profile
teachers.md with teachers_dir: teachers; profiles live in teachers/.
| Key | Description | |
|---|---|---|
title | Teacher name. | |
role | Title / main subject (alias subject). | |
avatar | Photo (alias image). | |
location | Shown as a chip. | |
email | mailto: chip. | |
subjects | Label list of tags. | |
socials | `Label\ | URL list (alias links`). |
teacher_page | true to render outside teachers/. |
The profile lists every course whose teacher: matches this file's basename.
Schools
Index / school page
schools.md with schools_dir: schools; schools live in schools/.
| Key | Description | |
|---|---|---|
title | School name. | |
location | Chip. | |
website | Link chip. | |
email | mailto: chip. | |
logo / cover | Logo + banner (gradient fallback). | |
description | Intro / card blurb. | |
accreditations | `Label\ | Note list (alias affiliations`). |
school_page | true to render outside schools/. |
The school page lists every course whose school: matches its basename.
Data on disk (no database)
| Data | Location |
|---|---|
| Course meta | content/pages/courses/<course>.md |
| Lessons | content/pages/courses/<course>/<lesson>.md |
| Enrollments | content/lms/enrollments/<course>.enroll.json |
| Reviews | content/lms/reviews/<course>.reviews.json |
| Payments | content/pages/products/_payments.md (shared) |
API summary
| Endpoint | Auth | Purpose |
|---|---|---|
POST /api/v2/lms/enroll | signed-in | Enroll (with student data). |
POST /api/v2/lms/reviews | signed-in | Add/replace a course review. |
POST /api/v2/lms/enroll/status | admin | Activate / mark paid. |
POST /api/v2/lms/enroll/remove | admin | Remove a student. |
Reachability
Add courses, teachers, schools to the sidebar exclusion list and their
*_dir keys to the aggregate keys. Excluding courses also hides every lesson
under courses/<course>/ from the sidebar. Surface pages from _nav.md:
- [Courses](/docs/courses)
- [Teachers](/docs/teachers)
- [Schools](/docs/schools)
- [My Courses](/docs/my-courses)