ESP32-S3 Bench Guide
ESP32-S3 N16R8 bench guide: live pinout, lessons, simulators and the bench log.
Interactive Year 9 Mathematics — lessons, worked examples and hundreds of practice questions.
Translation pending
EducationTypeScript · CSSGitHub @PunrunnerOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Interactive Year 9 Mathematics learning site by The Thai Guy
Translation pending
Taken from the repository README
Loading the full README.
Read the full README ↗An interactive Year 9 maths site. Students get lessons with hide/reveal worked solutions, fourteen kinds of self-marking practice question, interactive diagrams, scored quizzes and progress tracking — all running in their browser, with no login and no server.
This guide is for you, not for a developer. If you can edit a text file, you can add lessons and questions. You never need to touch the design or the code.
You need Node.js (the "LTS" version). Install it once.
Open a terminal in this folder and run:
npm install # once, the first time only
npm run dev # every time you want to work on the site
Then open http://localhost:4321 in your browser.
Leave npm run dev running while you work. Every time you save a file the browser
updates by itself. Press Ctrl+C in the terminal to stop it.
A note about OneDrive. This project lives in a OneDrive folder. OneDrive tries to sync the
node_modulesfolder, which contains tens of thousands of files and will slow your machine down. Right-clicknode_modules→ Free up space, or move the whole project somewhere outside OneDrive. Nothing innode_modulesneeds backing up —npm installrecreates it.
The site is free to host. These steps take about ten minutes, once.
cd "path/to/year9-maths"
git init
git add .
git commit -m "Year 9 Maths site"
Go to github.com/new, create a repository called
year9-maths, and leave every tick box unticked. GitHub then shows you two commands
— run them:
git remote add origin https://github.com/YOUR-USERNAME/year9-maths.git
git branch -M main
git push -u origin main
year9-maths repository.netlify.toml and fills the settings in for you. Check they say:
npm run builddistAfter a minute or two you get a link like
https://cheerful-marzipan-a1b2c3.netlify.app. That is your shareable link.
In Netlify: Site configuration → Change site name. Pick something like
thaiguy-year9-maths, and your link becomes
https://thaiguy-year9-maths.netlify.app.
Finally, open astro.config.mjs and put that address in the site: line, then push
the change (see below).
Prefer Vercel? It works just as well, and the project already includes a
vercel.json. Go to vercel.com/new, sign in with GitHub, importyear9-mathsand click Deploy — the settings fill in automatically. You get a link likehttps://year9-maths.vercel.app, and everygit pushupdates it. Put that address in thesite:line ofastro.config.mjs.
Prefer GitHub Pages? It also works. In
astro.config.mjsaddbase: '/year9-maths', then in your repository go to Settings → Pages and set the source to GitHub Actions. Netlify is simpler, which is why it is the recommendation.
The easy way — no terminal at all. Go to your repository on github.com, click into the file you want to change, click the pencil icon, edit, and click Commit changes. Netlify rebuilds automatically and the live site updates in about a minute.
The terminal way, if you have been editing on your computer:
git add .
git commit -m "Added the ratio and proportion topic"
git push
That is it. Netlify does the rest. Watch the progress on your Netlify dashboard.
year9-maths/
├── src/
│ ├── content/ ← EVERYTHING YOU EDIT IS IN HERE
│ │ ├── strands.json the three NSW strands
│ │ ├── topics/ one .json file per topic
│ │ ├── lessons/ one .md file per lesson, in topic folders
│ │ ├── exercises/ one .json file per practice set
│ │ └── quizzes/ one .json file per topic quiz
│ │
│ ├── components/ the exercise widgets and diagrams (leave alone)
│ ├── layouts/ page templates (leave alone)
│ ├── lib/ marking, progress, maths rendering (leave alone)
│ ├── pages/ the site's URLs (leave alone)
│ ├── styles/
│ │ ├── tokens.css ← colours, fonts and spacing live here
│ │ ├── global.css base styles
│ │ └── exercises.css question styles
│ └── content.config.ts the rules your content is checked against
│
├── scripts/ helper scripts (audit, tests)
├── public/ the favicon
└── netlify.toml hosting settings
You will spend all your time in src/content/. If you want to adjust a colour or
a font size, src/styles/tokens.css is the one place to do it — every part of the
site reads its values from there.
Create a file in src/content/topics/. The file name becomes the web address, so
use lower-case words joined by hyphens: ratio-proportion.json becomes
/topics/ratio-proportion/.
{
"title": "Ratio & Proportion",
"description": "Solving ratio problems, direct proportion and inverse proportion.",
"strand": "number-algebra",
"order": 11,
"difficulty": "core",
"objectives": [
"Share a quantity in a given ratio",
"Recognise and use direct proportion",
"Recognise and use inverse proportion"
],
"keywords": ["ratio", "proportion", "share", "scale"]
}
| Field | What it does |
|---|---|
title | Shown on the card and at the top of the page |
description | One line on the topic card |
strand | One of number-algebra, measurement-geometry, statistics-probability |
order | Chapter number — controls the order topics appear in |
difficulty | foundation, core or challenge |
objectives | "By the end of this topic you can…" list |
keywords | Extra words to help the search box find it |
extension | Optional. true leaves the topic out of the overall completion % |
All sixteen topics already exist, so you will mostly be adding lessons to them.
Create a Markdown file inside src/content/lessons/<topic-id>/. Name it with a number
first so the files sort sensibly: 01-solving-ratios.md.
A lesson has two parts: frontmatter (the settings between the --- lines) and the
body (the prose below it).
---
title: Solving Ratio Problems
topic: ratio-proportion
order: 1
minutes: 20
difficulty: core
summary: 'Sharing an amount in a given ratio, and working back from one part.'
objectives:
- 'Share a quantity in a given ratio'
- 'Find the whole when you know one part'
keyRules:
- title: 'Adding the parts'
formula: '\text{one part} = \frac{\text{total}}{\text{sum of the ratio parts}}'
body: 'Add the numbers in the ratio to find how many parts there are altogether.'
workedExamples:
- title: 'Sharing in a ratio'
problem: 'Share $\pounds 60$ in the ratio $2 : 3$.'
steps:
- explain: 'Add the parts of the ratio.'
maths: '2 + 3 = 5'
- explain: 'Divide the total by the number of parts.'
maths: '60 \div 5 = 12'
- explain: 'Multiply each share.'
maths: '2 \times 12 = 24, \qquad 3 \times 12 = 36'
answer: '$\pounds 24$ and $\pounds 36$'
visual:
widget: number-line
title: 'Try it'
caption: 'Drag the marker.'
config:
min: 0
max: 10
practice: ratio-01
---
## Why this matters
Write your explanation here in ordinary Markdown. Use `##` for headings, `-` for
bullet points, `**bold**` for emphasis, and `$x^2$` for maths.
Tables work too:
| Ratio | Parts | Each part |
| --- | --- | --- |
| $2:3$ | 5 | $\pounds 12$ |
You can drop an activity anywhere in the Markdown — straight after the paragraph it belongs to — with a line like this:
[[activity: first-sort]]
Then describe it under activities: in the frontmatter. Each one has a label (the small
red tag above it) and one of question, visual or reveal:
activities:
pi-predict:
label: 'Predict' # anything you like: Quick check, Your turn, Challenge…
question: # any of the 15 question types (see section 7)
type: truefalse
prompt: 'Is $\frac{22}{7}$ exactly equal to $\pi$?'
answer: false
explanation: 'They differ in the third decimal place.'
root-two:
label: 'Try it'
visual: # any interactive diagram (see section 11)
widget: number-line
title: 'Where does √2 live?'
caption: 'Drag the marker to where you think it sits.'
config: { min: 0, max: 3, step: 0.01, start: { x: 0.5 } }
negative-root:
label: 'Think about it'
reveal: # a question with the answer behind a button
prompt: 'Why does your calculator give an error for $\sqrt{-4}$?'
answer: 'Any number squared is positive or zero.'
You do not need to give inline questions an id — the activity name is used. Mix them
up: a lesson that opens with a Predict or Before we start question, and puts a
Quick check after each new idea, is far more engaging than one long read followed by
practice at the end. The three Integers, Powers & Roots lessons are worked examples of
this — each is built a different way.
| Field | Required? | What it does |
|---|---|---|
title | yes | The lesson heading |
topic | yes | Must match a topic file name exactly |
order | yes | Position inside the topic, starting at 1 |
summary | yes | One sentence, shown in the lesson list |
objectives | yes | At least one. Shown in the red box near the top |
minutes | no | Estimated time (default 15) |
difficulty | no | foundation, core or challenge |
keyRules | no | The rules and formulas panel |
workedExamples | no | Solutions that reveal one step at a time |
visual | no | An interactive diagram (see section 11) |
practice | no | The name of a file in exercises/, without .json |
summary: 'Finding $x$ when...'. Without the quotes the special characters can
confuse the file format. If your text itself contains an apostrophe, double it:
'the student''s answer'.Practice sets live in src/content/exercises/. One file per lesson, and the file name
is what you put in the lesson's practice: field.
The skeleton of every file:
{
"title": "Practice: solving ratios",
"intro": "Optional line of text above the questions.",
"questions": [
...questions go here...
]
}
Optional extras on the file:
"pick": 6 — show 6 random questions from the bank instead of all of them, and
give the student a New questions button."shuffle": true — show all the questions but in a random order.{
"id": "r1-q1",
"type": "numeric",
"prompt": "The question text. Supports $maths$ and **bold**.",
"hints": ["Shown one at a time when the student presses Hint.", "A second hint."],
"explanation": "The worked explanation, revealed after answering.",
"difficulty": "core",
"marks": 1,
"needsReview": false
}
id must be unique within its own file. hints, difficulty, marks and
needsReview are all optional.
Set "needsReview": true on any question you have not checked yet — it shows a
Draft chip on the page, and npm run audit lists them all for you.
Below is a complete, copy-paste example of every single type.
One right answer. Options are shuffled unless you say otherwise.
{
"id": "q1",
"type": "mcq",
"prompt": "What is the gradient of $y = 5x - 2$?",
"options": [
{ "id": "a", "text": "$5$" },
{ "id": "b", "text": "$-2$" },
{ "id": "c", "text": "$3$" }
],
"answer": "a",
"hints": ["Compare it with $y = mx + c$."],
"explanation": "The gradient is the number multiplying $x$, so **5**."
}
Add "shuffleOptions": false to keep your order — useful when an option says
"none of these".
Several right answers, with partial credit. A wrong tick cancels out a right one.
{
"id": "q2",
"type": "multi",
"prompt": "Select **every** line parallel to $y = 4x + 1$.",
"options": [
{ "id": "a", "text": "$y = 4x - 7$" },
{ "id": "b", "text": "$y = 4x$" },
{ "id": "c", "text": "$y = -4x + 1$" },
{ "id": "d", "text": "$y = x + 4$" }
],
"answers": ["a", "b"],
"explanation": "Parallel lines share a gradient, so any line with $m = 4$."
}
{
"id": "q3",
"type": "truefalse",
"prompt": "**Every square is a rectangle.**",
"answer": true,
"explanation": "True — a square meets every condition for a rectangle."
}
Rename the buttons with "labels": ["Rational", "Irrational"].
Accepts equivalent forms automatically: 0.75, 3/4 and 75% all match each other.
{
"id": "q4",
"type": "numeric",
"prompt": "Find the area of a circle with radius $4.5$ cm. Use $\\pi = 3.14$.",
"answer": 63.585,
"tolerance": 0.4,
"accept": ["63.59"],
"unit": "cm²",
"placeholder": "Answer",
"explanation": "$A = \\pi r^2 = 3.14 \\times 20.25 = 63.585$."
}
answer can be a number, or a string like "3/4", "2 1/2" or "1.5e8".tolerance allows rounding. 0.4 means anything within 0.4 counts.accept lists extra wordings you want to allow.unit is shown beside the box; the student does not type it.Mark each gap with [[name]] in the text, then describe it in blanks.
{
"id": "q5",
"type": "fill-blank",
"prompt": "Complete the rule.",
"text": "For independent events, $P(\\text{A and B}) = P(\\text{A}) \\;[[op]]\\; P(\\text{B})$, and all outcomes total [[total]].",
"blanks": [
{ "id": "op", "accept": ["multiply", "times", "x"], "size": 8 },
{ "id": "total", "accept": ["1", "one"], "size": 4 }
],
"explanation": "AND means multiply, and a full set of outcomes always totals 1."
}
Each gap is marked separately, so partial credit comes for free. size is roughly how
many characters wide the box should be.
Answers in any order. Give gaps the same group and the student can fill them in
either order — ideal for the two solutions of a quadratic:
"text": "$x =$ [[a]] or $x =$ [[b]]",
"blanks": [
{ "id": "a", "accept": ["-3"], "size": 4, "group": "roots" },
{ "id": "b", "accept": ["4"], "size": 4, "group": "roots" }
]
Checked by maths, not spelling — 12 + 3x and 3(x + 4) both pass for 3x + 12.
{
"id": "q6",
"type": "algebraic",
"prompt": "Find the nth term of $7,\\ 11,\\ 15,\\ 19,\\ \\dots$",
"answer": "4n + 3",
"variables": ["n"],
"placeholder": "e.g. 4n + 3",
"explanation": "The difference is 4, and $4n$ is 3 short, so **$4n + 3$**."
}
variables tells the marker which letters are unknowns. Use ["a", "b"] for two.^ for powers: n^2 means $n^2$. A symbol pad appears under the box
for phones."requireForm": true when the shape matters, such as "factorise" questions —
then an expanded answer is rejected even though it is equal.3xz/y
or 6pr and it is read as $3 \times x \times z \div y$. Just list every letter in
variables.{
"id": "q7",
"type": "match",
"prompt": "Match each term to its meaning.",
"left": [
{ "id": "l1", "text": "Theoretical probability" },
{ "id": "l2", "text": "Experimental probability" }
],
"right": [
{ "id": "r1", "text": "Worked out by reasoning" },
{ "id": "r2", "text": "Measured by running trials" }
],
"solution": { "l1": "r1", "l2": "r2" },
"explanation": "One is calculated; the other is measured."
}
Students can drag, or tap one answer and then tap its row — so it works on a phone and with a keyboard.
{
"id": "q8",
"type": "order",
"prompt": "Put the steps in the right order.",
"items": [
{ "id": "s1", "text": "Find the common difference" },
{ "id": "s2", "text": "Write down $dn$" },
{ "id": "s3", "text": "Compare with the sequence" },
{ "id": "s4", "text": "Add or subtract to match" }
],
"solution": ["s1", "s2", "s3", "s4"],
"explanation": "Find $d$, build the skeleton, compare, adjust."
}
solution lists the ids from first to last. Each item in the right place scores.
Each cell is either given (already filled in) or an answer (a gap to fill).
{
"id": "q9",
"type": "table",
"prompt": "Complete the table of values for $y = 2x - 1$.",
"columns": ["$x$", "$0$", "$1$", "$2$"],
"rows": [
[
{ "given": "$y$" },
{ "answer": "-1" },
{ "answer": "1" },
{ "answer": "3" }
]
],
"explanation": "Substitute each $x$ into $2x - 1$."
}
Cells take tolerance and accept just like numeric questions. Set
"rowHeader": false if the first column is not a heading.
Each step must be right before the next appears.
{
"id": "q10",
"type": "steps",
"prompt": "Work through this one step at a time.",
"scenario": "A cylinder has radius 6 cm and height 10 cm. Use $\\pi = 3.14$.",
"steps": [
{
"kind": "numeric",
"prompt": "Find the area of one circular end.",
"answer": 113.04,
"tolerance": 0.5,
"feedback": "$\\pi r^2 = 3.14 \\times 36$.",
"hint": "Use $\\pi r^2$ with $r = 6$."
},
{
"kind": "mcq",
"prompt": "How many circular ends does a closed cylinder have?",
"options": [
{ "id": "a", "text": "Two" },
{ "id": "b", "text": "One" }
],
"answer": "a",
"feedback": "Two — top and bottom."
},
{
"kind": "algebraic",
"prompt": "Write the formula for the curved surface area.",
"answer": "2*pi*r*h",
"variables": ["r", "h"],
"feedback": "Circumference times height."
}
],
"explanation": "Two ends plus the curved surface."
}
Each step's kind is numeric, mcq or algebraic, and takes the same fields as the
matching full question type. feedback and hint are optional.
The student clicks the right part of a diagram.
{
"id": "q11",
"type": "hotspot",
"prompt": "**Click the hypotenuse.**",
"figure": "right-triangle",
"config": { "a": "3 cm", "b": "4 cm", "c": "5 cm" },
"answer": "hypotenuse",
"explanation": "The side opposite the right angle."
}
The diagrams available, and what you can click on each:
figure | Things you can click (answer) |
|---|---|
right-triangle | hypotenuse, leg-a, leg-b, right-angle, vertex-a, vertex-b, vertex-c |
circle-parts | centre, radius, diameter, circumference, chord |
line-graph | y-intercept, x-intercept, origin, rise, run, point-on-line |
distance-time | fastest, stationary, slowest, start, finish |
cylinder | curved-surface, top-face, bottom-face, height, radius |
tree-diagram | branch-a, branch-b, branch-aa, branch-ab, branch-ba, branch-bb |
parallel-lines | p-tl, p-tr, p-bl, p-br (top crossing), q-tl, q-tr, q-bl, q-br (bottom crossing) |
scatter-graph | p0, p1, p2, … — one per point, in the order you list them |
Use a list for answer when more than one region is acceptable:
"answer": ["radius", "diameter"].
line-graph takes "config": { "m": 2, "c": -2 }; tree-diagram takes
"config": { "first": ["H", "T"], "second": ["H", "T"], "p1": "½", "p2": "½" }.
parallel-lines shows two parallel lines cut by a transversal. In the region names, t/b
mean above/below the line and l/r mean left/right of the crossing line. Shade the
known angle with "config": { "given": "p-tr", "givenLabel": "a" } — then ask for its
corresponding (q-tr), alternate (q-bl), co-interior (q-br) or vertically opposite
(p-bl) angle.
scatter-graph takes "config": { "points": [[3, 2.1], [4, 3.5], …], "xLabel": "…", "yLabel": "…", "xRange": [2, 10], "yRange": [0, 10] } and optionally
"line": [[x1, y1], [x2, y2]] for a line of best fit. Great for "click the outlier".
The student moves sliders or drags points until they match a target.
{
"id": "q12",
"type": "manipulable",
"prompt": "Move the sliders so the line matches $y = 3x - 4$.",
"widget": "line-mc",
"config": { "start": { "m": 1, "c": 0 } },
"target": { "m": 3, "c": -4 },
"tolerance": 0.01,
"explanation": "$m = 3$ and $c = -4$."
}
widget | Values you can target |
|---|---|
line-mc | m, c |
parabola | a, b, c |
number-line | x |
pythagoras | a, b |
prism | l, w, h |
polygon-angles | n |
enlargement | k, ox, oy |
spinner | spins (use a large tolerance — see below) |
area-model | a, b — the rectangle for $(x + a)(x + b)$ |
bearing | b — a three-figure bearing, 0 to 359 |
histogram | f0, f1, f2, … — the frequency of each class |
axis-trick | lo, hi — where the $y$-axis starts and ends |
Leave target out entirely and the question becomes a free exploration that always
counts as complete.
For the spinner, "target": { "spins": 100 }, "tolerance": 400 means "do at least
100 spins" — the tolerance is deliberately wide so more spins still count.
config options: number-line takes min, max, step, majorStep, label;
pythagoras and prism take min, max, step, unit; spinner takes sectors
(a list of names) and focus (which one to report on).
More config options:
number-line with "inequality": true adds an arrow direction and an open/closed
circle. Target all three: "target": { "x": 9, "dir": 1, "closed": 1 } means
$x \ge 9$ (dir is 1 right / -1 left; closed is 1 for ≤ ≥, 0 for < >).polygon-angles with "inscribed": true draws the circle and radii.bearing takes distance and unit to show how far East/West and North/South a
journey goes.histogram takes boundaries (e.g. [10, 20, 30, 40, 50]), freqs, maxFreq,
label, "polygon": true for a frequency polygon, and "bars": false to hide the bars.
It shows the estimated mean, modal class and median class live.axis-trick takes values, labels and unit — a bar chart for misleading-graph lessons."start": { … } to choose its starting values. Make sure the target
is different from the start, or the question is marked right before the student
touches anything.Cards start mixed up; the student drags (or taps) each into its group. Write each group with the cards that belong in it — the site shuffles them. Partial credit is automatic.
{
"id": "q15",
"type": "sort",
"prompt": "Sort these numbers.",
"groups": [
{ "id": "rat", "text": "Rational", "items": [{ "id": "a", "text": "$0.3$" }, { "id": "b", "text": "$-7$" }] },
{ "id": "irr", "text": "Irrational", "items": [{ "id": "c", "text": "$\\pi$" }, { "id": "d", "text": "$\\sqrt{2}$" }] }
],
"explanation": "0.3 and −7 can be written as fractions; π and √2 cannot."
}
Use two or three groups (four at most on phones). Every card id must be different.
Not marked — a revision tool. Students flip, sort into "got it" and "review again", and can reshuffle.
{
"id": "q13",
"type": "flashcards",
"prompt": "Flip through these until you can recall each one.",
"cards": [
{ "id": "c1", "front": "Area of a circle", "back": "$A = \\pi r^2$" },
{ "id": "c2", "front": "Circumference", "back": "$C = 2\\pi r$" }
],
"explanation": "These two cover most circle questions."
}
A burst of short questions against a clock, for fluency.
{
"id": "q14",
"type": "drill",
"prompt": "Quick-fire: expected frequency.",
"durationSec": 90,
"count": 8,
"pool": [
{ "id": "d1", "prompt": "100 trials, $P = 0.3$?", "accept": ["30"] },
{ "id": "d2", "prompt": "50 trials, $P = 0.4$?", "accept": ["20"] },
{ "id": "d3", "prompt": "80 trials, $P = \\frac{1}{4}$?", "accept": ["20"] },
{ "id": "d4", "prompt": "60 trials, $P = \\frac{1}{6}$?", "accept": ["10"] }
],
"explanation": "Every one is trials $\\times$ probability."
}
count items are drawn at random from pool each run, so it is different every time.
Put at least twice as many in the pool as count. Items can take a tolerance.
Hints are revealed one at a time, so write them getting progressively more helpful. Three is usually plenty:
"hints": [
"What is the common difference?",
"The difference is 4, so start with $4n$.",
"Compare $4, 8, 12$ with $7, 11, 15$ — what has been added?"
]
Explanations appear after answering, whether the student was right or wrong. The best ones do three things: give the answer, show the working, and name the mistake people usually make.
"explanation": "$r = 10 \\div 2 = 5$, so $A = 3.14 \\times 25 = 78.5$ cm². Using 10 instead of 5 gives four times too much — the classic slip."
Every question must have an explanation. The site will not build without one.
Maths goes between dollar signs and uses LaTeX, the standard maths notation.
| You want | You type |
|---|---|
| $x^2$ | $x^2$ |
| $x_1$ | $x_1$ |
| $\frac{3}{4}$ | $\frac{3}{4}$ |
| $\sqrt{16}$ | $\sqrt{16}$ |
| $\sqrt[3]{27}$ | $\sqrt[3]{27}$ |
| $\pi$ | $\pi$ |
| $\times \div \pm$ | $\times \div \pm$ |
| $\le \ge \ne \approx$ | $\le \ge \ne \approx$ |
| $45\degree$ | $45\degree$ |
| plain words in maths | $\text{speed} = 5$ |
| a whole line, centred | $$a^2 + b^2 = c^2$$ |
Inside a .json file every backslash must be doubled.
In a .md lesson file | In a .json question file |
|---|---|
$\frac{1}{2}$ | "$\\frac{1}{2}$" |
$\pi r^2$ | "$\\pi r^2$" |
Markdown lessons take single backslashes. JSON question files take double. If maths shows up as red code on the page, a missing second backslash is almost always why.
Create a file in src/content/quizzes/ named after its topic —
ratio-proportion.json. The questions use exactly the same format as practice.
{
"title": "Ratio and proportion quiz",
"topic": "ratio-proportion",
"description": "Ten questions drawn from a bank of fourteen.",
"pick": 10,
"timeLimitSec": 900,
"passMark": 70,
"questions": [ ... ]
}
pick — how many to draw each attempt. Make the bank bigger than pick so a retry
gives a genuinely different paper.timeLimitSec — for the optional timed mode. Students choose whether to use it.passMark — the percentage needed to pass.marks on a question — worth more than one mark. Partial-credit types award a
fraction of them.A quiz marks everything at the end, not as the student goes, and then shows every question with the right answer and your explanation.
Do not use flashcards or drill in a quiz — they run their own loop and do not
fit a marked paper. Every other type is fine.
The Exam practice page does not use quiz questions. It builds a Cambridge-style written paper from the exam questions described next.
Each topic has a file in src/content/exam/, named after the topic. A question has an
optional stem (the shared text or data at the top) and parts (a), (b)… A part can
hold sub-parts (i), (ii)… Every part carries its marks and its row of the mark scheme:
{
"topic": "quadratics",
"questions": [
{
"id": "q3",
"calculator": "either",
"difficulty": "challenge",
"stem": "A rectangle has length $(x + 5)$ cm and width $(x - 2)$ cm. Its area is 30 cm$^2$.",
"parts": [
{
"label": "a",
"prompt": "Show that $x^2 + 3x - 40 = 0$.",
"marks": 2,
"answer": "$(x + 5)(x - 2) = 30 \\Rightarrow x^2 + 3x - 40 = 0$",
"partial": ["M1 for $(x + 5)(x - 2) = 30$", "A1 for correct expansion to the given equation"]
},
{
"label": "b",
"prompt": "Solve $x^2 + 3x - 40 = 0$ to find $x$.",
"marks": 2,
"answerPrefix": "$x =$",
"check": { "kind": "numeric", "answer": 5 },
"answer": "5",
"partial": ["M1 for $(x + 8)(x - 5)$"]
}
],
"needsReview": false
}
]
}
| Field | What it does |
|---|---|
calculator | "calculator" questions only appear on calculator papers. "non-calculator" and "either" appear on both |
marks | Shown as [2] beside the answer line |
answer | The Answer column of the mark scheme |
qualifier | Shorthand after the answer: "oe", "cao", "FT their (a)", "isw" |
partial | The Partial marks column. Start each one with the mark code, e.g. "M1 for …", "B1 for …", "SC1 for …" |
check | Lets the site mark the final answer: numeric (with tolerance, accept), algebraic (with variables, requireForm) or text (a list of accept strings) |
answerPrefix / answerSuffix | Text either side of the answer line, e.g. "$x =$", "cm$^2$" |
answerLines + checks | Several answer lines, e.g. ["$x =$", "$y =$"], with one check each |
anyOrder | The answer lines can be filled in either order (the two roots of a quadratic) |
Leave out check when the answer cannot be typed: a reason, a sketch, a "show that".
The student then marks that part themselves against the scheme. When a final answer is
wrong, the student reads the partial marks and awards any method marks their working
earned, just as an examiner would.
Which check? Use text for answers whose form matters: standard form, "simplify",
"factorise completely", coordinates. The algebraic check accepts anything equivalent,
so on its own it would pass x(6x + 9) for "factorise $6x^2 + 9x$ completely".
npm run keys puts every check through the real marker, so run it after you edit.
Add one to a lesson with the visual block in the frontmatter:
visual:
widget: pythagoras
title: 'Drag the triangle'
caption: 'Try $a = 3$, $b = 4$ and see what the hypotenuse comes out as.'
config:
min: 1
max: 10
step: 1
unit: cm
The twelve available widgets are listed in the manipulable table in section 7. In a lesson there is no target and nothing to mark — it is purely something to play with.
The same widget can be used as a question by giving it a target (type 12).
Three commands, all safe to run any time:
npm run audit # what is in the site, and whether anything is broken
npm run keys # marks every answer key with the real marker — catches typos
npm test # checks the answer-marking rules still work
npm run build # the full check — run this before pushing
npm run check-maths # after a build: finds maths showing as raw code, like $\frac{1}{2}$
npm run audit prints a summary like this:
Lessons 61
Questions total 798
mcq 13 █████████████
numeric 50 ██████████████████████████████
...
All 15 question types are in use.
✓ Every reference resolves and all question ids are unique.
It also lists anything you flagged with "needsReview": true.
npm run keys takes every question's own answer and marks it exactly as the site
would. If it reports a problem, the site would mark a correct student wrong — fix it
before publishing. Add a name to check one topic only: npm run keys -- quadratics.
npm run build is the strict one. If your content has a mistake, the build stops
and tells you the file and the field, for example:
[InvalidContentEntryDataError] exercises → ratio-01
questions.3.explanation: Required
That means the fourth question in ratio-01.json has no explanation. Fix it and
build again. This is the safety net: a broken question can never reach your students.
| What you see | What it means |
|---|---|
questions.2.answer: Required | The third question in that file is missing its answer |
Invalid discriminator value. Expected 'mcq' | 'multi' | ... | A type is misspelt |
topic: Reference to topics invalid | The topic: name does not match a file in topics/ |
lesson ... wants practice set "x", which does not exist | The practice: name has no matching file in exercises/ |
| Maths shows as red code | A missing double backslash in a .json file |
Unexpected token } in JSON | A stray or missing comma — every item needs a comma except the last |
bad indentation of a mapping entry | A tab in the frontmatter. Use spaces |
| A lesson does not appear | Check topic: matches, and that the file ends in .md |
| Nothing updates in the browser | Stop npm run dev with Ctrl+C and start it again |
Progress lives in each student's own browser. They can clear it themselves at My progress → Reset progress. Nothing is stored anywhere else, so there is nothing for you to manage — and nothing of theirs that you can see.
Colours, fonts, spacing and rounding are all defined once in
src/styles/tokens.css. Change a value there and it updates everywhere. The site is
white by default; students can switch to dark with the toggle in the header, and their
choice is remembered.
The site respects prefers-reduced-motion, works fully from the keyboard, and never
signals right or wrong with colour alone — there is always an icon and a word.