diff --git a/.agents/skills/ofb/SKILL.md b/.agents/skills/ofb/SKILL.md
index c5c7605..ac599df 100644
--- a/.agents/skills/ofb/SKILL.md
+++ b/.agents/skills/ofb/SKILL.md
@@ -74,7 +74,7 @@ bun run build # production: JS + Zola + pagefind → public/
- **Component UI** only (`pagefind-component-ui.js`), not legacy `pagefind-ui.js`.
- Index runs **after** `zola build`. Production: `bun run build`. Dev: `bun run index:search` copies bundle to `static/pagefind/` (gitignored) for `zola serve`.
-- v1 indexes **live chapter pages only** (`data-pagefind-body` on `templates/math/chapter.html`); home, about, catalog, credits, and other site pages are excluded.
+- v1 indexes **live chapter pages only** (`data-pagefind-body` on `templates/chapter.html`); home, about, catalog, credits, and other site pages are excluded.
Detailed checklists: [curriculum-data.md](curriculum-data.md). Human-facing guide: [CONTRIBUTING.md](../../../CONTRIBUTING.md).
@@ -105,16 +105,17 @@ Detailed checklists: [curriculum-data.md](curriculum-data.md). Human-facing guid
Prerequisites: **only** `graph.edges` (required, global DAG, cross-strand OK). Longest-path levels. No implicit strand-list chains. List order ≠ map order.
-## Chapter content (current pattern)
+## Chapter content (colocated folder)
-Live math example:
+Live chapters live under `content/{subject}/{slug}/`:
-1. `content/math/quadratic-equations/_index.md` — front matter, `template = "math/chapter.html"`, `[extra] chapter_id`
-2. `templates/partials/math/quadratic-equations-core-content.html` — prose + KaTeX (supplement partial for history, derivation, etc.)
-3. `templates/math/chapter.html` — `{% if section.extra.chapter_id == "…" %}` includes partial
-4. Optional: `frontend/src/components/quadratic-explorer.tsx` + mount in `main.tsx`
+1. `data/{subject}-curriculum.json` — catalog metadata (`status`, `curriculums`, `graph.edges`)
+2. `_index.md` — front matter only (`template = "chapter.html"`, `[extra] subject`, `chapter_id`, `strand`)
+3. `core.html` (required), optional `supplement.html`, `assets/`, `widgets/*.tsx`, optional `chapter.scss`
+4. `bun run sync:chapters` merges HTML into `_index.md`, copies assets to `static/chapters/…`, generates `frontend/src/generated/chapter-widgets.ts`
+5. `templates/chapter.html` renders `{{ section.content | safe }}`; widgets use `data-widget` + auto-mount in `main.tsx`
-**Pain point:** each new chapter needs a template `if` branch until a generic chapter loader exists.
+Reference: `content/math/quadratic-equations/`.
## Verify before PR
diff --git a/.agents/skills/zola/zola-pagefind.md b/.agents/skills/zola/zola-pagefind.md
index d76ad3e..13229bb 100644
--- a/.agents/skills/zola/zola-pagefind.md
+++ b/.agents/skills/zola/zola-pagefind.md
@@ -42,7 +42,7 @@ bun run build # build:js + zola build + index:search
| Source | Indexed? |
|--------|----------|
-| `templates/math/chapter.html` → `` | Yes |
+| `templates/chapter.html` → `` | Yes |
| Home, about, catalog, credits, contributing, `/search/` | No |
| Catalog chapter cards (Solid + JSON) | No |
| Planned chapters (no `content/` page) | No |
@@ -51,10 +51,10 @@ New subject chapter templates must include `data-pagefind-body` on the main less
### Per-chapter markup
-[`themes/openfreebooks/templates/math/chapter.html`](../../../themes/openfreebooks/templates/math/chapter.html):
+[`themes/openfreebooks/templates/chapter.html`](../../../themes/openfreebooks/templates/chapter.html):
```html
-
+
…
diff --git a/.gitignore b/.gitignore
index 9d8fcfe..ca877d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,3 +24,9 @@ Thumbs.db
# Vite build output (bun run build:js → zola copies into public/)
themes/openfreebooks/static/
+
+# Chapter sync output (bun run sync:chapters)
+frontend/src/generated/
+data/_generated/
+static/css/chapters/
+static/chapters/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index de69391..e6e1b57 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -20,32 +20,30 @@ Thank you for helping grow free, open textbooks.
**Browse the catalog locally:** `bun run build:js && zola serve` → [http://127.0.0.1:1111/catalog/](http://127.0.0.1:1111/catalog/)
-**Search locally:** After `bun run build:js`, run `bun run index:search` once (or again after changing chapter HTML), then `zola serve`. Try [http://127.0.0.1:1111/search/](http://127.0.0.1:1111/search/) or press ⌘K / Ctrl+K. Pagefind indexes **live chapter pages only** (not home, about, catalog, or credits). Without `index:search`, the search UI loads but returns no results.
+**Search locally:** After `bun run build:js`, run `bun run index:search` once (or again after changing chapter HTML), then `zola serve`. Try [http://127.0.0.1:1111/search/](http://127.0.0.1:1111/search/) or press ⌘K / Ctrl+K.
---
## How the catalog fits together
```text
-data/catalog.json → subjects in the sidebar + filter chips (DSE, IB, …)
-data/{subject}-curriculum.json → strands, chapters, curriculum tags, graph.edges
-content/{subject}/{slug}/ → actual chapter page (when status is "live")
-themes/.../partials/... → chapter HTML content (today)
+data/{subject}-curriculum.json → catalog list + map (status, curriculums, graph.edges)
+content/{subject}/{slug}/ → everything for the live page (one folder)
+ _index.md, core.html, supplement.html, assets/, widgets/, chapter.scss
```
-- **Catalog** (`/catalog/`) — **List** (numbered cards; live = whole card links) and **Map** (`?view=tree`, Mermaid; live = title link only with `→` and hover underline).
-- **`/math/`** redirects to `/catalog/?subject=math`.
-- **Curriculum names** (DSE, IB, …) appear only in the catalog — not inside chapter text ([spec.md](spec.md)).
+- **Catalog** (`/catalog/`) — list and map (`?view=tree`). Curriculum names (DSE, IB, …) appear only in the catalog — not inside chapter prose ([spec.md](spec.md)).
- Every chapter needs a **`description`** in curriculum JSON (map clamps to three lines).
+- **`bun run sync:chapters`** (also run automatically by `bun run build:js`) merges `core.html` + `supplement.html` into `_index.md` for Zola. Edit the HTML files, not the merged body in `_index.md`.
### Map graph rules (summary)
| Rule | Detail |
|------|--------|
-| Edges | **Only** `graph.edges` — no automatic chain from strand `chapters[]` order |
+| Edges | **Only** `graph.edges` — strand `chapters[]` order does not create arrows |
| Meaning | Each edge is a **required** prerequisite (`from` → `to`) |
| Scope | Cross-strand edges allowed |
-| Layout | **Longest-path** levels; siblings at the same level stack vertically in a column |
+| Layout | **Longest-path** levels; siblings stack vertically |
| DAG | No cycles — build throws if the graph cycles |
| `tier` | Optional `foundation` (default) or `non-foundation` (Extension badge) |
@@ -53,11 +51,8 @@ themes/.../partials/... → chapter HTML content (today)
## Add a planned chapter (roadmap)
-Use this when the chapter is not written yet but should appear in the catalog.
-
1. Open `data/{subject}-curriculum.json` (e.g. `data/math-curriculum.json`).
-2. Find the right **strand** (topic group).
-3. Add a chapter object:
+2. Add a chapter object under the right **strand**:
```json
{
@@ -70,136 +65,98 @@ Use this when the chapter is not written yet but should appear in the catalog.
}
```
-(`tier` is optional — omit for foundation topics; `non-foundation` shows an **Extension** badge.)
-
-4. Use a **kebab-case** `slug` (letters, numbers, hyphens only).
-5. Set `curriculums` to every syllabus tag that applies. Tags must exist in `data/catalog.json` → `curriculums`.
-6. Run `zola build` and check `/catalog/?subject=math` (or your subject).
-
-No `content/` files needed for planned chapters.
+3. Use **kebab-case** slugs. No `content/` folder needed for planned chapters.
---
## Publish a live chapter
-A live chapter needs **catalog data** and a **page**.
-
### 1. Catalog entry
-In `data/{subject}-curriculum.json`, set `"status": "live"` and include a `description` (see [planned chapter](#add-a-planned-chapter-roadmap)).
+In `data/{subject}-curriculum.json`, set `"status": "live"` and include `description`, `curriculums`, and optional `tier`.
-### 2. Zola section
+### 2. Chapter folder
-Create `content/{subject}/{slug}/_index.md`:
+Create `content/{subject}/{slug}/`:
+
+```text
+content/math/your-chapter-slug/
+├── _index.md # page metadata (see below)
+├── core.html # required — main lesson HTML
+├── supplement.html # optional — history, checkpoints, extra demos
+├── chapter.scss # optional — chapter-specific styles
+├── assets/ # optional — images (synced to static/chapters/…)
+└── widgets/ # optional — Solid widgets (auto-mounted)
+ └── my-widget.tsx # default export; use data-widget="my-widget" in HTML
+```
+
+**`_index.md` front matter** (page fields only — catalog fields stay in JSON):
```md
+++
title = "Your chapter title"
description = "One sentence for search and previews — no curriculum names here."
-template = "math/chapter.html"
+template = "chapter.html"
weight = 10
[extra]
+subject = "math"
chapter_id = "your-chapter-slug"
+strand = "Number & Algebra"
+++
```
-- `slug` in the path must match `slug` in the JSON file.
-- `chapter_id` must match the slug (used by the template).
-- `weight` controls order within the section (lower = earlier).
+### 3. HTML and widgets
-### 3. Chapter HTML content
+- Use shared classes: `book-prose`, `book-prose__heading`, `book-callout`, etc. Copy from `content/math/quadratic-equations/`.
+- **Do not** mention DSE, IB, or other curriculum names in chapter HTML.
+- Mount widgets with `` (filename without `.tsx` = `data-widget` value).
+- Reference synced assets with root-relative paths: `/chapters/math/your-chapter-slug/image.png` (chapter HTML is not processed as Tera)
-Today, chapter bodies live as HTML partials under the theme, not Markdown:
+### 4. Build and check
-1. Add `themes/openfreebooks/templates/partials/math/your-chapter-slug-content.html` with the lesson HTML.
-2. In `themes/openfreebooks/templates/math/chapter.html`, include it when `chapter_id` matches (chapter content is inside `` for search indexing):
-
-```html
-{% if section.extra.chapter_id == "your-chapter-slug" %}
-{% include "partials/math/your-chapter-slug-content.html" %}
-{% endif %}
+```bash
+bun run build:js # sync:chapters + Vite bundle
+zola serve
```
-Use existing classes: `book-prose`, `book-prose__heading`, `book-callout`, etc. Copy from `quadratic-equations-core-content.html`.
+- `/catalog/?subject=math` — chapter appears and links correctly.
+- `/math/your-chapter-slug/` — page renders.
+- `bun run build` — full production build.
-**Do not** mention DSE, IB, or other curriculum names in the chapter body.
-
-### 4. Interactive widgets (optional)
-
-If the chapter needs a Solid widget (like the quadratic explorer):
-
-1. Add a component under `frontend/src/components/`.
-2. Mount it in `frontend/src/main.tsx` when a mount element exists.
-3. Add `` in the chapter template branch.
-
-Then run `bun run build:js`.
-
-### 5. Check
-
-- `/catalog/?subject=math` — chapter appears; live chapters link correctly.
-- `/math/your-chapter-slug/` — page renders; breadcrumb goes to catalog.
-- `bun run build` completes without errors.
+No theme edits, no `main.tsx` edits, no per-chapter template branches.
---
## Chapter graph (branches)
-The **Map** tab (`?view=tree`) shows chapters as a directed graph (strand = column, arrows = prerequisites).
-
-- Prerequisites are **only** declared in `graph.edges` (strand list order does not create arrows).
-- For **branches** or **merges**, add edges:
+Add edges in `data/{subject}-curriculum.json`:
```json
"graph": {
"edges": [
- { "from": "quadratic-equations", "to": "sequences-series" },
- { "from": "quadratic-equations", "to": "functions-graphs" },
- { "from": "functions-graphs", "to": "linear-programming" },
- { "from": "sequences-series", "to": "linear-programming" }
+ { "from": "quadratic-equations", "to": "sequences-series" }
]
}
```
-Rules:
-
-- `from` and `to` are chapter **slugs**.
-- The graph must **not contain cycles**.
-- Edges may cross strands.
-
-Open `/catalog/?subject=math&view=tree` to verify. Use Ctrl/Cmd + wheel on the map to zoom.
+`from` / `to` are chapter **slugs**. Graph must be acyclic. Verify at `/catalog/?subject=math&view=tree`.
---
## Add a new subject
-Example: adding **Science**.
-
-1. **`data/catalog.json`** — add to `subjects`:
-
- ```json
- { "id": "science", "name": "Science" }
- ```
-
-2. **`data/science-curriculum.json`** — create with `title`, `strands`, and chapters (copy structure from `math-curriculum.json`).
-
-3. **`themes/openfreebooks/templates/catalog.html`** — wire the new file into `#catalog-data` (see `math` block).
-
-4. **`content/science/`** — add `_index.md` when you have a section.
-
-5. **Homepage** — optional card in `themes/openfreebooks/templates/index.html`.
-
-6. Build and test `/catalog/?subject=science`.
+1. `data/catalog.json` — add to `subjects`.
+2. Create `data/{id}-curriculum.json` (copy structure from `math-curriculum.json`).
+3. Wire the subject in `themes/openfreebooks/templates/catalog.html` (until auto-wire lands).
+4. Add `content/{subject}/` when you have chapters.
---
## Add a curriculum label
-Example: adding **GCSE**.
-
-1. Add `"GCSE"` to `curriculums` in `data/catalog.json`.
-2. Tag chapters with `"GCSE"` in their `curriculums` arrays.
-3. Add a badge style in `themes/openfreebooks/sass/_catalog.scss` (copy `.catalog-badge--igcse` pattern).
-4. Add a mapping in `frontend/src/lib/catalog-badge.ts` → `curriculumBadgeClass()` (or rely on the default style).
+1. Add the label to `curriculums` in `data/catalog.json`.
+2. Tag chapters in their `curriculums` arrays.
+3. Add badge style in `themes/openfreebooks/sass/_catalog.scss` and mapping in `frontend/src/lib/catalog-badge.ts`.
---
@@ -207,22 +164,23 @@ Example: adding **GCSE**.
```bash
bun install
-bun run build:js # after frontend/ changes
+bun run sync:chapters # after content/ chapter changes
+bun run build:js # sync + Vite
zola serve
-bun test # catalog map generation (catalog-to-mermaid)
+bun test
bun run build
```
-- Do **not** commit `themes/openfreebooks/static/` (Vite output). Run `bun run build:js` before `zola build` or `zola serve`.
+- Do **not** commit `themes/openfreebooks/static/` (Vite output).
- Do **not** commit `public/`.
---
## Pull requests
-- Keep changes focused (one subject or a few chapters per PR is fine).
+- Keep changes focused.
- Run `bun run build` before opening the PR.
-- Describe what you added: subject, chapters, live vs planned, and which curricula apply.
+- Describe subject, chapters, live vs planned, and curricula.
Questions? Open a [GitHub discussion](https://github.com/hananoshikayomaru/openfreebooks) or issue.
@@ -230,4 +188,4 @@ Questions? Open a [GitHub discussion](https://github.com/hananoshikayomaru/openf
## For maintainers & agents
-Machine-oriented detail: [.agents/skills/ofb/SKILL.md](.agents/skills/ofb/SKILL.md) and [.agents/skills/ofb/curriculum-data.md](.agents/skills/ofb/curriculum-data.md).
+Machine-oriented detail: [.agents/skills/ofb/SKILL.md](.agents/skills/ofb/SKILL.md).
diff --git a/README.md b/README.md
index 823ef5a..41f5905 100644
--- a/README.md
+++ b/README.md
@@ -103,9 +103,9 @@ These issues make contributions harder than they should be. PRs that fix any ite
### Chapters & content
-- [ ] **Generic chapter template** — drop per-chapter `{% if chapter_id %}` branches; load `partials/{subject}/{slug}-content.html` by convention.
-- [ ] **Strand kicker from data** — chapter header still hardcodes “Number & Algebra”; should come from strand in curriculum JSON.
-- [ ] **Subject-agnostic chapter template** — breadcrumb/catalog URL and `load_data` path should use section path, not `math` only.
+- [x] **Colocated chapter folders** — `content/{subject}/{slug}/` with `sync:chapters`, generic `chapter.html`, auto widget registry.
+- [x] **Strand kicker from front matter** — `[extra] strand` in `_index.md`.
+- [ ] **Subject-agnostic catalog load in chapter template** — breadcrumb still loads `math-curriculum.json` literally until more subjects go live.
- [ ] **Markdown chapters (optional)** — README/spec disagree on format; if staying HTML, document clearly everywhere.
- [ ] **Defuddle “copy as Markdown”** — spec calls for it on every page; not implemented.
diff --git a/bun.lock b/bun.lock
index 2cf57c7..6da812f 100644
--- a/bun.lock
+++ b/bun.lock
@@ -13,7 +13,8 @@
"three": "^0.184.0",
},
"devDependencies": {
- "pagefind": "^1.3.0",
+ "pagefind": "^1.5.0",
+ "sass": "^1.89.2",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite-plugin-solid": "^2.11.6",
@@ -147,6 +148,34 @@
"@pagefind/windows-x64": ["@pagefind/windows-x64@1.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg=="],
+ "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="],
+
+ "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="],
+
+ "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA=="],
+
+ "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg=="],
+
+ "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng=="],
+
+ "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ=="],
+
+ "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg=="],
+
+ "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA=="],
+
+ "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA=="],
+
+ "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ=="],
+
+ "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg=="],
+
+ "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q=="],
+
+ "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g=="],
+
+ "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
+
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="],
@@ -289,6 +318,8 @@
"caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="],
+ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
+
"commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
@@ -383,6 +414,8 @@
"delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="],
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
"dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
"domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
@@ -419,10 +452,16 @@
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
+ "immutable": ["immutable@5.1.5", "", {}, "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A=="],
+
"import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
"is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
@@ -455,6 +494,8 @@
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
+ "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
+
"node-releases": ["node-releases@2.0.44", "", {}, "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ=="],
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
@@ -477,6 +518,8 @@
"postcss": ["postcss@8.5.14", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
+ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
+
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
"rollup": ["rollup@4.60.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.4", "@rollup/rollup-android-arm64": "4.60.4", "@rollup/rollup-darwin-arm64": "4.60.4", "@rollup/rollup-darwin-x64": "4.60.4", "@rollup/rollup-freebsd-arm64": "4.60.4", "@rollup/rollup-freebsd-x64": "4.60.4", "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", "@rollup/rollup-linux-arm-musleabihf": "4.60.4", "@rollup/rollup-linux-arm64-gnu": "4.60.4", "@rollup/rollup-linux-arm64-musl": "4.60.4", "@rollup/rollup-linux-loong64-gnu": "4.60.4", "@rollup/rollup-linux-loong64-musl": "4.60.4", "@rollup/rollup-linux-ppc64-gnu": "4.60.4", "@rollup/rollup-linux-ppc64-musl": "4.60.4", "@rollup/rollup-linux-riscv64-gnu": "4.60.4", "@rollup/rollup-linux-riscv64-musl": "4.60.4", "@rollup/rollup-linux-s390x-gnu": "4.60.4", "@rollup/rollup-linux-x64-gnu": "4.60.4", "@rollup/rollup-linux-x64-musl": "4.60.4", "@rollup/rollup-openbsd-x64": "4.60.4", "@rollup/rollup-openharmony-arm64": "4.60.4", "@rollup/rollup-win32-arm64-msvc": "4.60.4", "@rollup/rollup-win32-ia32-msvc": "4.60.4", "@rollup/rollup-win32-x64-gnu": "4.60.4", "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g=="],
@@ -487,6 +530,8 @@
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+ "sass": ["sass@1.99.0", "", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q=="],
+
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"seroval": ["seroval@1.5.4", "", {}, "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw=="],
diff --git a/content/math/quadratic-equations/_index.md b/content/math/quadratic-equations/_index.md
index cd595e0..85ae197 100644
--- a/content/math/quadratic-equations/_index.md
+++ b/content/math/quadratic-equations/_index.md
@@ -1,8 +1,235 @@
+++
title = "Quadratic equations"
description = "The discriminant, nature of roots, and the graph of y = ax² + bx + c."
-template = "math/chapter.html"
+template = "chapter.html"
weight = 1
[extra]
+subject = "math"
chapter_id = "quadratic-equations"
+strand = "Number & Algebra"
+++
+
+
+
+ A quadratic equation in one unknown has the form \(ax^2 + bx + c = 0\), where
+ \(a \neq 0\). You can solve these by factorisation, the quadratic
+ formula, or by reading information from the graph of \(y = ax^2 + bx + c\).
+
+
+
The discriminant
+
+ The discriminant \(\Delta\) (delta) is defined below. It tells you how many
+ real roots the equation has without fully solving it.
+
+
+
+
Definition
+
\[ \Delta = b^2 - 4ac \]
+
+
+
Nature of roots
+
+
+
+
+
Discriminant
+
Nature of roots
+
Graph of \(y = ax^2 + bx + c\)
+
+
+
+
+
\(\Delta > 0\)
+
Two distinct real roots
+
Parabola cuts the x-axis twice
+
+
+
\(\Delta = 0\)
+
One repeated real root (equal roots)
+
Vertex touches the x-axis
+
+
+
\(\Delta < 0\)
+
No real roots
+
Parabola lies entirely above or below the x-axis
+
+
+
+
+
+
+ When \(\Delta \geq 0\), the quadratic formula gives the roots:
+ \[ x = \frac{-b \pm \sqrt{\Delta}}{2a} \]
+
+
+
Try it yourself
+
+ Use the explorer below to change \(a\), \(b\), and \(c\). Watch how \(\Delta\) and the
+ parabola change, and read off the nature of the roots.
+
+
+
+
+
+
+
History
+
+ People have worked on problems that lead to quadratic equations for thousands of years.
+ Babylonian clay tablets from around 1800 BCE record area and length puzzles — for example,
+ finding the sides of a rectangle when you know its area and perimeter. Those situations
+ produce equations in one unknown whose highest power is two.
+
+
+
+
+
+ A Babylonian clay tablet (about 1800 BCE) with a problem equivalent to finding the
+ sides of a rectangle from its area and perimeter.
+
+
+
+
+ Early methods were often geometric: you imagine completing a rectangle to make a square,
+ which is the same idea behind completing the square today. In the 9th century,
+ al-Khwarizmi wrote systematic rules for many types of quadratic problems,
+ sorted by which coefficients were present. He did not use modern symbols like \(x^2\), but
+ the logic matches what we still teach.
+
+
+ During the Renaissance, mathematicians such as Cardano and
+ Tartaglia pushed further into solving cubics and quartics. Along the way they
+ met equations whose discriminants were negative — no real roots, yet the algebra still
+ demanded answers. That tension helped open the door to complex numbers, a
+ topic you will meet in a later chapter.
+
+
+
Derivation
+
+ The quadratic formula comes from rearranging \(ax^2 + bx + c = 0\) and completing the square.
+ Below is a guided derivation; routine algebra between steps is left for you to check.
+
+
+ Start with \(a \neq 0\) and divide every term by \(a\):
+ \[ x^2 + \frac{b}{a}x + \frac{c}{a} = 0. \]
+ Move the constant to the right:
+ \[ x^2 + \frac{b}{a}x = -\frac{c}{a}. \]
+
+
+ The expression \(x^2 + \frac{b}{a}x\) is almost a perfect square. Add and subtract
+ \(\left(\frac{b}{2a}\right)^2\) on the left (equivalently, add that square to both sides):
+ \[ x^2 + \frac{b}{a}x + \left(\frac{b}{2a}\right)^2 = -\frac{c}{a} + \left(\frac{b}{2a}\right)^2. \]
+ The left side factors as a square:
+ \[ \left(x + \frac{b}{2a}\right)^2 = \frac{b^2 - 4ac}{4a^2}. \]
+
+
+ Take square roots (when the right-hand side is non-negative you get real solutions):
+ \[ x + \frac{b}{2a} = \pm \frac{\sqrt{b^2 - 4ac}}{2a}. \]
+ Solve for \(x\):
+
+
+
Quadratic formula
+
+ \[ x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} \]
+
+
+
+ The quantity under the square root is exactly the discriminant:
+ \(\Delta = b^2 - 4ac\). So the formula is \(x = \frac{-b \pm \sqrt{\Delta}}{2a}\) whenever
+ \(\Delta \geq 0\). If \(\Delta > 0\) you get two distinct real roots; if \(\Delta = 0\) the
+ \(\pm\) gives the same root twice; if \(\Delta < 0\) there is no real square root and the
+ equation has no real solutions — matching the nature-of-roots table above.
+
+
+
Checkpoints
+
+ Pause and think before moving on. There are no answers on this page — discuss with a
+ classmate or write your reasoning in a notebook.
+
+
+
+
Checkpoint
+
+ In the explorer, keep \(a\) and \(b\) fixed and slowly change \(c\) so that \(\Delta\) goes
+ from positive to zero. What happens to the two x-intercepts on the graph?
+
+
+
+
+
Checkpoint
+
+ Can \(ax^2 + bx + c = 0\) with \(a \neq 0\) have three distinct real solutions? Why or why not?
+
+
+
+
+
Checkpoint
+
+ Suppose \(a > 0\) and \(\Delta < 0\). Where does the entire parabola \(y = ax^2 + bx + c\)
+ sit relative to the x-axis?
+
+
+
+
+
Checkpoint
+
+ A ball is thrown straight upward from ground level. Without using numbers, describe a
+ situation where \(\Delta > 0\) for height versus time, and another where \(\Delta = 0\).
+
+
+
+
+
Checkpoint
+
+ After completing the square you had \(\left(x + \frac{b}{2a}\right)^2 = \frac{\Delta}{4a^2}\).
+ Why must the sign of \(\Delta\) match whether the parabola crosses the x-axis?
+
+
+
+
Applications
+
+
Projectile motion
+
+ Near Earth’s surface, height \(h\) (in metres) of an object thrown upward often follows
+ \[ h = -\tfrac{1}{2}gt^2 + v_0 t + h_0, \]
+ where \(t\) is time in seconds, \(g \approx 9.8\) is gravitational acceleration,
+ \(v_0\) is initial upward speed, and \(h_0\) is starting height. This is a quadratic in
+ \(t\). Setting \(h = 0\) asks when the object is at ground level; the solutions are the
+ landing times. The discriminant tells you whether there are two crossing times
+ (\(\Delta > 0\)), one (\(\Delta = 0\)), or none (\(\Delta < 0\)) — for example if the
+ model never reaches the ground in the time you care about.
+
+
+
+
+
Profit and pricing
+
+ A shop might find that when it charges \(p\) dollars per item, revenue is \(p\) times
+ quantity sold, while cost has a fixed part plus a cost per item. Profit (revenue minus cost)
+ is often a quadratic in \(p\) that opens downward — there is a best price at the vertex.
+ For example, with profit \(\Pi\) in dollars,
+ \[ \Pi(p) = -2p^2 + 40p - 150. \]
+ Setting \(\Pi = 0\) asks for break-even prices; \(\Delta\) tells you whether two
+ break-even prices exist, one, or none.
+
+
+
+
+
Area with fixed perimeter
+
+ A rectangle with perimeter \(P\) can be described by width \(x\) and length
+ \(\frac{P}{2} - x\). Its area is
+ \[ A(x) = x\left(\frac{P}{2} - x\right) = -\!x^2 + \frac{P}{2}x, \]
+ a downward-opening parabola. The largest area occurs at the vertex, not at the extremes
+ \(x = 0\) or \(x = \frac{P}{2}\) (which give zero area).
+
+
+
+
diff --git a/content/math/quadratic-equations/assets/babylonian-quadratic-tablet.png b/content/math/quadratic-equations/assets/babylonian-quadratic-tablet.png
new file mode 100644
index 0000000..e1c49dd
Binary files /dev/null and b/content/math/quadratic-equations/assets/babylonian-quadratic-tablet.png differ
diff --git a/themes/openfreebooks/templates/partials/math/quadratic-equations-core-content.html b/content/math/quadratic-equations/core.html
similarity index 95%
rename from themes/openfreebooks/templates/partials/math/quadratic-equations-core-content.html
rename to content/math/quadratic-equations/core.html
index ea9f3d8..ece67f4 100644
--- a/themes/openfreebooks/templates/partials/math/quadratic-equations-core-content.html
+++ b/content/math/quadratic-equations/core.html
@@ -57,3 +57,5 @@
parabola change, and read off the nature of the roots.
+
+
diff --git a/themes/openfreebooks/templates/partials/math/quadratic-equations-supplement-content.html b/content/math/quadratic-equations/supplement.html
similarity index 95%
rename from themes/openfreebooks/templates/partials/math/quadratic-equations-supplement-content.html
rename to content/math/quadratic-equations/supplement.html
index 216b7b5..babc295 100644
--- a/themes/openfreebooks/templates/partials/math/quadratic-equations-supplement-content.html
+++ b/content/math/quadratic-equations/supplement.html
@@ -10,7 +10,7 @@
-
+
Profit and pricing
@@ -147,7 +147,7 @@
break-even prices exist, one, or none.
-
+
Area with fixed perimeter
@@ -158,5 +158,5 @@
\(x = 0\) or \(x = \frac{P}{2}\) (which give zero area).
List — numbered cards; live chapters link from the whole card.
Map (?view=tree) — Mermaid map with strand columns; live chapters link from the title only (→ + hover underline).
@@ -67,15 +66,13 @@ themes/.../partials/... → chapter HTML bodies (today)
Publish a live chapter
-
A live chapter needs catalog JSON and a Zola section.
+
A live chapter needs a catalog JSON entry and a folder under content/{subject}/{slug}/.
1. Catalog entry
-
Set "status": "live" and include description in the curriculum JSON.
-
2. Zola section
-
Create content/{subject}/{slug}/_index.md with chapter_id matching the slug.
-
3. Chapter HTML
-
Add themes/openfreebooks/templates/partials/math/your-slug-content.html and wire it in math/chapter.html. Copy patterns from quadratic-equations-core-content.html.
-
4. Optional widget
-
Add a Solid component under frontend/src/components/ and mount it in main.tsx. Run bun run build:js.
+
Set "status": "live" in data/{subject}-curriculum.json with description and curriculums.