CourseApp is a system-installed CLI that fires up and builds a private Obsidian vault and a public MkDocs site that work in tandem to product a course site building kit. You use Obsidian to create course content in Markdown files and then have MkDocs publish them to a GitHup Pages site.
To use it, courseapp is installed once on your system and is bundles the MkDocs toolchain. Each course it creates is just content + a local config.yml — no per-site virtual environment.
This guide takes you from nothing all the way to two GitHub repos — a private vault and a public, auto-deploying site.
- Private source — an Obsidian vault folder. Only notes whose frontmatter says
publish: trueare compiled; everything else stays private. Notebooks are authored in-vault via the JupyMD plugin (its own Jupyter venv). - Public target — a standalone MkDocs (Material) repo. It holds only what the CLI compiled — the committed
docs/— and deploys to GitHub Pages. - The bridge — a gitignored
config.ymlinside the public repo that maps your local vault + attachments paths. It never leaves your machine.
PRIVATE PUBLIC
vault/ ──(courseapp publish)──▶ site/docs/ ──(courseapp deploy)──▶ GitHub Pages
(Obsidian) (committed) (live site)
Prerequisites: Python ≥ 3.10, git, and pipx. No Homebrew or system-Python changes — pipx puts courseapp in its own isolated environment and drops a launcher on your PATH.
# install pipx if you don't have it
python3 -m pip install --user pipx
pipx ensurepath # adds ~/.local/bin to PATH (open a new terminal after)
# install courseapp (works on macOS, Linux, Windows)
pipx install git+https://github.com/ontoligent/courseappVerify in a fresh terminal:
which courseapp # → ~/.local/bin/courseapp
courseapp --help # lists: create, publish, serve, deployDeveloping courseapp itself? Clone the repo and
pipx install -e ./courseappinstead — code edits then take effect live.
A course is two directories:
| Dir | What it is | Privacy |
|---|---|---|
| vault | The Obsidian vault you author in. You publish from a subfolder of it (e.g. pub/). |
private |
| site | The MkDocs repo that gets deployed. Holds compiled docs/ + config. |
public |
A tidy convention:
~/Repos/Courses/my-course/
vault/ ← Obsidian vault root (private)
pub/ ← the publishable subfolder
site/ ← public MkDocs repo (public)
One command stamps out both sides:
courseapp create \
--title "My Course" \
--vault ~/Repos/Courses/my-course/vault \
--folder pub \
--target ~/Repos/Courses/my-course/site \
--git-vault| Flag | Meaning |
|---|---|
--title |
Site name shown in the header. |
--vault |
The vault root (the folder Obsidian opens). |
--folder |
Subfolder inside the vault whose notes get published (e.g. pub). |
--target |
Where the public site repo is created. |
--git-vault |
Also git init the vault (recommended — you'll push it in step 6). |
--no-vault-venv |
Skip building the Jupyter authoring venv (see below). |
Leave off any of the first four and create will prompt you for it.
The authoring venv. By default create builds a Jupyter venv at <vault>/.venv (jupytext + matplotlib/numpy/pandas/etc.) so the JupyMD plugin has a working kernel. This runs pip install and is slow — add --no-vault-venv to skip it and set it up later from <vault>/vault-requirements.txt.
Public site (--target):
site/
mkdocs.yml # theme + site_name
config.yml # vault paths (gitignored — local only)
requirements.txt # CI installs courseapp from this
README.md
.gitignore
.github/workflows/deploy.yml # CI: deploy on push to main
Private vault (--vault):
vault/
pub/
.jupytext.toml # md ⇄ ipynb pairing for JupyMD
index.md # seed note (publish: true)
attachments/
vault-requirements.txt
.venv/ # authoring venv (unless --no-vault-venv)
config.ymlis the link between the two. It records your vault paths and is gitignored, so it never leaks into the public repo.
Work from the site directory:
cd ~/Repos/Courses/my-course/site
courseapp publish # compile the vault's pub/ notes → docs/
courseapp serve # live preview at http://127.0.0.1:8000Edit notes in Obsidian, re-run courseapp publish, refresh the browser. Only notes with publish: true in their frontmatter are included. When it looks right:
git add docs/
git commit -m "Update notes"
git push # CI deploys the docs/ you just reviewedWhat you saw at localhost:8000 is exactly what goes live — the deploy server only ever sees the compiled docs/, never your private vault.
Prefer to deploy by hand (no CI)?
courseapp deploy # build docs/ → push straight to GitHub PagesTo move notes from an existing vault into the new one:
# 1. copy the publishable notes
cp -R /path/to/old-vault/pub/. ~/Repos/Courses/my-course/vault/pub/
# 2. copy attachments
cp -R /path/to/old-vault/attachments/. ~/Repos/Courses/my-course/vault/attachments/Then make sure each note you want public has publish: true in its frontmatter, and run courseapp publish && courseapp serve to check. Your old vault stays untouched, so you can migrate gradually and compare.
Two repos with opposite visibility: the vault is private, the site is public.
The vault holds unpublished notes, so its repo must be private.
create already initialized the vault as a git repo (with --git-vault) and wrote a .gitignore that excludes the authoring .venv/, .DS_Store, and per-machine Obsidian state. So just commit:
cd ~/Repos/Courses/my-course/vault
git add -A
git commit -m "Initial vault"Create an empty private repo on GitHub (web UI → New → Private, or gh repo create my-course-vault --private), then:
git remote add origin https://github.com/<you>/my-course-vault.git
git push -u origin mainThe site was already git init'd by create, with docs/ committed and config.yml gitignored. Make sure your latest build is committed:
cd ~/Repos/Courses/my-course/site
courseapp publish
git add -A
git commit -m "Initial site"Create an empty public repo on GitHub, then push:
git remote add origin https://github.com/<you>/my-course-site.git
git push -u origin mainPushing to main triggers .github/workflows/deploy.yml, which installs courseapp (from this public repo — no edits needed, the ref is baked into requirements.txt) and runs courseapp deploy. That builds docs/ and pushes a gh-pages branch.
Finally, turn on Pages: repo Settings → Pages → Source: "Deploy from a branch" → gh-pages / / (root) → Save. After the first successful Action your site is live at:
https://<you>.github.io/my-course-site/
From here on, your whole workflow is: edit in Obsidian →
courseapp publish→ commitdocs/→git push. CI redeploys automatically. The vault and the live site never touch each other.
You can author reveal.js slide decks in Obsidian and have them appear, fully interactive, on the published site — embedded in a normal lesson page.
-
Author and preview decks in Obsidian with the Slides Extended plugin (the maintained successor to Advanced Slides). What you preview in Obsidian is what ships.
-
Export each deck to HTML. The export is a folder — an
.htmlentry plus its reveal.js assets — so give every deck its own folder anywhere in the vault (e.g.attachments/Lecture01/). Don't drop a deck's HTML loose among other files, or its neighbors get published with it. -
Embed it from any
publish: truenote using a normal Obsidian embed:![[Lecture01.html]]
-
Publish.
courseapp publishcopies the deck's folder intodocs/slides/<folder>/and replaces the embed with a responsive 16:9 iframe plus an "Open slides fullscreen ↗" link. The relative path is adjusted automatically for how deeply the note is nested.
Decks are served as static assets — no MkDocs plugin or extra dependency is involved, so this keeps working regardless of the MkDocs/Material plugin landscape.
| Command | Run from | Does |
|---|---|---|
courseapp create |
anywhere | Scaffold a new vault + site pair. |
courseapp publish |
site dir | Compile the vault's published notes into docs/. |
courseapp serve |
site dir | Local live-reloading preview at :8000. |
courseapp deploy |
site dir | Build docs/ and push to GitHub Pages by hand. |
command not found: courseapp→ runpipx ensurepathand open a new terminal.~/.local/binmust be on yourPATH.courseappruns an old version /createmissing → an active virtualenv is shadowing the pipx install.deactivate, open a fresh terminal, and checkwhich courseapppoints at~/.local/bin/courseapp.❌ config.yml is missing→ runpublish/serve/deployfrom the site directory (the one withmkdocs.yml), not the vault.- CI runs but the site 404s → make sure GitHub Pages is set to the
gh-pagesbranch (step 6b), and that the Action finished successfully (Actions tab). - CI fails pushing
gh-pages→ in the site repo, Settings → Actions → General → Workflow permissions must allow Read and write.