Status: v1.0 — stable, repo-agnostic, ready to reuse.
Canonical source: github.com/reh3376/mdemg → scripts/tutorial-forge/
Reference consumer: the MDEMG onboarding tutorial at https://reh3376.github.io/mdemg/ (live demo of every feature below).
Config-driven, repo-agnostic framework that renders a markdown deck into a static HTML tutorial package.
For an AI coding agent: start with
FSD.md. It is the structured, agent-oriented reference (purpose, non-goals, file-by-file contracts, decision tables, failure signatures). This README is the human-oriented quick-start.
Any repo can point it at its own deck + config and get the same professional presentation layer:
- Persistent left-margin TOC + Graphs sidebar (REQUIRED feature; see FSD §15). Fixed-position sidebar listing section headings AND every diagram, with scroll-sync highlighting the current section. Readers can jump anywhere in a long tutorial without scrolling.
- Pre-rendered Mermaid diagrams — every
mermaidfence is rendered to SVG at build time via headless Puppeteer, wrapped in an interactive viewer (toolbar + wheel-zoom + drag-pan + fullscreen). No client-side Mermaid runtime in the shipped bundle. - Hand-authored SVG passthrough — any SVG files under your
assetsDirland in the output beside the generated ones and get the same viewer treatment. - Acronym expansion on first use — the first N occurrences of each known acronym become "Expansion (ACRONYM)" automatically. Industry-standard dictionary ships with the framework; extend or override per repo.
- Heading anchors + TOC-safe IDs — every heading gets a stable
slug-based ID. Explicit
<!-- .slide: id="..." -->directives can pin specific IDs for anchor-link stability across edits. - Speaker notes become collapsible
<details>— anynote:-prefixed trailing block in a slide becomes a<details class="speaker-notes">. - Drift breadcrumb for CI gating — the build writes
SOURCE_SHA256.txtwith the SHA of each configured source file; a consumer-side verifier (seescripts/verify_tutorial_embed.pyin mdemg for the reference impl) fails CI when the deck was edited without re-running the sync.
# From inside your repo, after copying scripts/tutorial-forge/ into place:
cd scripts/tutorial-forge
npm installThe dependencies (marked, mermaid, puppeteer, highlight.js) install into
scripts/tutorial-forge/node_modules/. Puppeteer downloads a bundled Chromium
on first install (one-time, ~150 MB).
Create tutorial.config.json at your repo root:
{
"deck": "docs/my-tutorial.md",
"output": "build/tutorial-static",
"title": "My Project Tutorial"
}Build:
node scripts/tutorial-forge/cli.mjs build --config tutorial.config.jsonThe output directory is a self-contained static site — serve it from any
static host (GitHub Pages, Netlify, nginx, python -m http.server, embedded
in a binary via //go:embed, etc.).
A ready-to-run demo lives at example/ — a tiny 5-section
deck with 2 Mermaid diagrams and the minimum config (just deck, output,
title; no overrides). It exercises every default: TOC + Graphs sidebar,
scroll-sync, pre-rendered Mermaid, acronym expansion, the interactive SVG
viewer. From the forge directory:
cd example
node ../cli.mjs build --config tutorial.config.json
cd dist && python3 -m http.server 8000
# open http://localhost:8000/If the demo produces a left-margin TOC with 5 Sections + 2 Graphs and
every acronym expands on first use, your scripts/tutorial-forge/
install is healthy.
Every path in the config is resolved relative to the config file's directory (absolute paths work too).
tutorial-forge treats your markdown as one long document. These conventions let you add structure without leaving plain markdown:
A line containing just --- splits the source into logical sections,
rendered with a subtle <hr class="slide-break" /> separator.
Add a slide directive before a heading to pin its anchor ID:
<!-- .slide: id="my-stable-anchor" -->
## Heading text
Content...Any TOC link using #my-stable-anchor stays valid even if the heading text
changes later.
Trailing note: block after the main content of a section becomes a
collapsible <details>:
## Section
Main content for the reader.
note:
Internal notes for the presenter. These start hidden; readers click to expand.Standard fenced ```mermaid blocks get pre-rendered to SVG and
wrapped in the interactive viewer:
```mermaid
flowchart TD
A[Source] --> B[Transform]
B --> C[Output]
```Author-side tips that improve rendered quality:
- Prefer
flowchart TDoverLRfor diagrams with long labels — wide-aspect SVGs (viewBox width > 3× height) crush text. TD lays out vertically and lets labels breathe. - Quote labels containing
(,),/,&or newlines:A["RRF fusion (1/(k+rank))"]. The renderer auto-wraps most unquoted cases but explicit quoting is more robust. - Use
<br/>for in-label line breaks (auto-converted to Mermaid's native\n).
Render the deck to a static HTML package at config.output. Overwrites the
output directory in place.
node scripts/tutorial-forge/cli.mjs build --config tutorial.config.jsonDry-run the acronym transform; prints what would change in the deck source
file. Add --write to persist the expansion back to config.deck:
# Preview what the transform would change
node scripts/tutorial-forge/cli.mjs expand-acronyms --config tutorial.config.json
# Persist the expansion to the deck source
node scripts/tutorial-forge/cli.mjs expand-acronyms --config tutorial.config.json --writeNote: build also runs acronym expansion in-memory on every build; --write
is only needed if you want the expanded form in the source file itself.
The transform is idempotent — running it against an already-expanded deck
is a no-op.
<config.output>/
├── index.html the deck as one long readable document
├── favicon.ico from config.iconPath or framework default
├── SOURCE_SHA256.txt drift breadcrumb (SHA of each configured input)
└── assets/
├── css/
│ ├── slides-overrides.css from config.cssPath or framework default
│ └── zenburn.min.css highlight.js code theme (framework-shipped)
├── js/
│ └── svg-viewer.js from config.svgViewerPath or framework default
└── svg/
├── mermaid-01.svg … one per ```mermaid fence, in source order
└── <hand-authored>.svg … merged from config.assetsDir if present
Add a verifier that fails CI when the deck was edited without re-running the
sync. See scripts/verify_tutorial_embed.py in the mdemg repo for a working
reference — it reads tutorial.config.json, re-computes the SHA of each
source file, and compares against SOURCE_SHA256.txt.
Wire into your Makefile:
.PHONY: sync-tutorial-embed verify-tutorial-embed
sync-tutorial-embed:
@node scripts/tutorial-forge/cli.mjs build --config tutorial.config.json
verify-tutorial-embed:
@python3 scripts/verify_tutorial_embed.py \
|| (echo "DRIFT: run 'make sync-tutorial-embed' and commit the result."; exit 1)And run verify-tutorial-embed on every PR.
Point config.output at a directory under internal/, then embed it:
import "embed"
//go:embed all:tutorial_templates/staged
var tutorialFS embed.FSServe it from an HTTP handler (see mdemg's internal/api/handlers_tutorial.go
for the reference impl: 503 when disabled, .. path-traversal rejection,
longest-prefix routing).
If you don't provide an override, the framework uses:
- CSS:
scripts/tutorial-forge/templates/default.css(dark theme, 1400px max-width centered, 18/16px type, interactive.svg-figurestyling, collapsible speaker-notes, zenburn code highlighting) - SVG viewer:
scripts/tutorial-forge/templates/svg-viewer.js(vanilla JS, zero deps, toolbar[+][−][⟳][⤢], wheel-zoom at cursor, drag-pan, touch pinch, double-click reset, idempotent + MutationObserver-safe) - Favicon:
scripts/tutorial-forge/templates/favicon.ico(the tiny generic icon reveal-md ships, vendored so reveal-md is not a dependency) - Acronym dictionary:
scripts/tutorial-forge/defaults/acronyms.json(~55 industry-standard terms)
From inside the TARGET repo (the one that wants a tutorial):
bash <(curl -fsSL https://raw.githubusercontent.com/reh3376/mdemg/main/scripts/tutorial-forge/install-into.sh)The installer:
- Clones a shallow copy of mdemg (framework code only, not the mdemg deck).
- Copies
scripts/tutorial-forge/into your repo verbatim. - Scaffolds
tutorial.config.jsonat your repo root if missing. - Scaffolds
docs/tutorial.mdas a starter deck if missing. - Appends sensible
.gitignoreentries. - Prints the exact next-steps.
Then from your repo root:
cd scripts/tutorial-forge && npm install # one-time (~150 MB Chromium)
# edit docs/tutorial.md with your content
node scripts/tutorial-forge/cli.mjs build --config tutorial.config.json
# serve build/tutorial-static/ with any static hostJust paste this at the end of a message:
Install tutorial-forge into any repo:
bash <(curl -fsSL https://raw.githubusercontent.com/reh3376/mdemg/main/scripts/tutorial-forge/install-into.sh)
Live demo: https://reh3376.github.io/mdemg/
- Copy
scripts/tutorial-forge/from mdemg into your target repo. - Run
cd scripts/tutorial-forge && npm installonce. - Create
tutorial.config.jsonat your repo root pointing at your deck and output locations. - Add Makefile targets (
sync-tutorial-embed,verify-tutorial-embed) or equivalent in your build system. - Author your deck in markdown with standard conventions (headings, lists,
tables,
mermaidblocks,note:speaker-notes).
Future: the forge may land as its own repo via git subtree split --prefix=scripts/tutorial-forge + published to npm; for v1.0 it ships as a
drop-in directory under mdemg.
Same as the containing repository.
{ // REQUIRED "deck": "docs/my-tutorial.md", "output": "build/tutorial-static", // Shell / branding (optional) "title": "My Tutorial", // <title> text; default "Tutorial" "iconPath": "docs/assets/favicon.ico", // default: framework favicon // Overrides (optional — framework ships sensible defaults) "cssPath": "docs/overrides.css", // override the dark-theme CSS "svgViewerPath": "docs/viewer.js", // override the zoom/pan viewer JS // Hand-authored assets (optional) // Everything under this dir is recursively merged into <output>/assets/, // layering over generated mermaid-NN.svg files (same filename collides // deterministically — the user's hand-authored file wins). "assetsDir": "docs/assets", // Acronyms (optional) "acronyms": { "expandCount": 1, // first-N occurrences → "Expansion (ACRONYM)" // every subsequent occurrence becomes a // link to the auto-generated glossary "extra": { // per-repo additions (merged into defaults) "FOO": "Full Of Options", "BAR": "Baz And Related" }, "disabled": ["API", "CLI"], // drop these from the dict entirely // (useful for project-internal names // you don't want auto-expanded) "glossary": { "enabled": true, // auto-emit a Glossary section at the // end of the deck listing every used // acronym. A floating "← Return to // Previous" button lets readers jump // back from a glossary entry. "label": "Glossary" } }, // Mermaid (optional) "mermaid": { "theme": "default", // "default" (light) or "dark" "skipRender": false // if true, keep ```mermaid blocks as // code fences (no SVG pre-rendering) }, // Persistent TOC + Graphs sidebar (REQUIRED; opt-out at your own risk) "toc": { "enabled": true, // default: true; sidebar always on "minLevel": 1, // headings H{min}..H{max} go in TOC "maxLevel": 2, "label": "Contents", // sidebar heading "graphs": { "enabled": true, // list every .svg-figure as a jump link "label": "Graphs" } }, // Frontmatter (optional) "stripFrontmatter": true // strip leading --- YAML --- block (default true) }