Interactive Quizzes
mdsvr renders ```quiz fenced blocks as interactive, self-grading quizzes. Questions, options, and answers are written as JSON; the reader sees a form, clicks Check answers, and gets instant feedback with a score. Results are stored per-user in localStorage — no backend required, works in static exports too.
Live Demo
Everything below is a real, working quiz — select answers and grade them right here.
Exam mode — answer everything, then check it all at once. Covers all four question types:
mdsvr (or npx mdsvr) at the folder — serving is the default action.```quiz blocks only work in .md files, not .mdx..mdx it compiles to the <Quiz> component.localStorage under mdsvr-quiz:<id> — per-browser, no server needed.Practice mode — each question has its own Check button and locks after grading:
mdsvr-quiz:<id> in the browser — works in static exports too.id).id, mdsvr hashes the payload — edit the quiz, get a new hash, new stats.Quick Example
```quiz
{
"id": "asyncio-basics",
"title": "Quick check",
"mode": "exam",
"questions": [
{
"type": "single",
"question": "Which function is the standard entrypoint for a coroutine?",
"options": ["`asyncio.run()`", "`asyncio.exec()`", "`asyncio.start()`"],
"answer": 0,
"explanation": "`asyncio.run()` has been the recommended API since Python 3.7."
},
{
"type": "true-false",
"question": "`async`/`await` is syntax owned by the asyncio library.",
"answer": false,
"explanation": "It is language syntax; asyncio is just one consumer."
}
]
}
```
Quiz Fields
| Field | Type | Default | Description |
|---|---|---|---|
id |
string |
auto-hash | Stable identifier for score history. When omitted, mdsvr hashes the payload — editing the quiz resets stored stats. |
title |
string |
"Quiz" |
Title shown in the quiz header. |
mode |
"exam" | "practice" |
"exam" |
exam: one Check answers button grades everything. practice: each question has its own check button for instant feedback. |
questions |
array |
required | At least one question (see types below). |
Question Types
| Type | Required fields | answer |
UI |
|---|---|---|---|
single |
question, options (≥2), answer |
number — 0-based index of correct option |
radio |
multiple |
question, options (≥2), answer |
number[] — all correct indices |
checkbox |
true-false |
question, answer |
boolean |
True/False |
self-check |
question, answer |
string — model answer |
reveal + self-grade |
Every question may also have an optional explanation (inline markdown), shown after grading.
Question text, options, explanations, and self-check model answers support inline markdown — `code`, **bold**, *italic*, links. Raw HTML is not allowed.
Scoring
- All-or-nothing per question — a
multiplequestion only counts if the exact correct set is selected. self-checkquestions count toward the total; the learner reports correct/incorrect themselves.- The result panel shows
correct/total, a percentage, a per-type breakdown, and history (Best: x/y · n attempts). - Stats are stored under
mdsvr-quiz:<id>inlocalStorage. Give the quiz a stableidif you want history to survive content edits.
Validation
--validate-md checks every ```quiz block against the schema and reports invalid-quiz errors with the fence’s line number — malformed JSON, missing fields, and out-of-range answer indices are all caught before publish. Invalid blocks render as an inline error callout instead of breaking the page.
In MDX
The same ```quiz fence works in .mdx files. You can also embed a quiz directly with the <Quiz> component:
<Quiz source='{"questions": [{"type": "true-false", "question": "Sky is blue.", "answer": true}]}' />
Disable it per-site with "mdx.components": { "Quiz": false } — quiz fences then render as plain code blocks.
Notes & Limitations
- No JavaScript: questions and options still render as a readable form; grading just does nothing.
- Search: quiz JSON is fenced code, so questions and answers do not appear in the search index.
- EPUB export:
mdsvr-to-epubconverts quiz blocks into a static numbered Q&A with an answer key.