diff --git a/.agents/skills/ofb/SKILL.md b/.agents/skills/ofb/SKILL.md index 75e2cf3..26150a0 100644 --- a/.agents/skills/ofb/SKILL.md +++ b/.agents/skills/ofb/SKILL.md @@ -33,6 +33,7 @@ frontend/ → catalog-app, Mermaid map view, site chrome ```bash bun install bun run build:js # after frontend/ changes +bun run generate:site-metadata # regenerate static/sitemap.xml + static/feed.xml from content + git log bun run index:search # once per dev session (copies public/pagefind → static/pagefind) zola serve # http://127.0.0.1:1111 bun run build # production: JS + Zola + pagefind → public/ @@ -59,10 +60,20 @@ bun run build # production: JS + Zola + pagefind → public/ | New subject (sidebar) | `data/catalog.json` → `subjects[]`; `data/{id}-curriculum.json` (catalog template auto-loads by `id`) | | New chapter (planned) | `{subject}-curriculum.json` strand entry; optional `graph.edges` | | New chapter (live) | Above + `content/{subject}/{slug}/_index.md` + HTML partial + `chapter.html` wiring + `main.tsx` widget mount if interactive | +| Sitemap/feed coverage | `scripts/generate-site-metadata.ts`, `static/sitemap.xml`, `static/feed.xml`, `package.json` build script | | Homepage subject card | `themes/.../templates/index.html` (not yet data-driven) | | Browse / nav URL | `zola.toml` `[extra] browse_url` + `base.html` site-config + `main.tsx` fallback | | Search UI / index | `base.html` (Pagefind assets + modal), `content/search.md`, `_search.scss`; `bun run index:search` for dev | +## Sitemap / feed auto-generation + +- `bun run generate:site-metadata` generates: + - `static/sitemap.xml` (all user-facing built pages + live chapters) + - `static/feed.xml` (RSS 2.0 for pages + live chapters) +- URL timestamps come from `git log -1` on content-source paths. If a page/chapter has no commit yet, generator falls back to filesystem mtime. +- Build pipeline runs this automatically before `zola build` via `package.json`. +- Include only resolvable pages; planned chapters without live content are excluded. + ## Search (Pagefind) | Piece | Path | diff --git a/content/math/laws-of-indices/_index.md b/content/math/laws-of-indices/_index.md new file mode 100644 index 0000000..8ac17da --- /dev/null +++ b/content/math/laws-of-indices/_index.md @@ -0,0 +1,234 @@ ++++ +title = "Laws of indices" +description = "Apply index laws to simplify expressions with integer and rational exponents." +template = "chapter.html" +weight = 12 +[extra] +subject = "math" +chapter_id = "laws-of-indices" +strand = "Number & Algebra" ++++ + +
+

+ An index (or exponent) tells you how many times a base is used as a factor. + For example, \(2^4 = 2 \times 2 \times 2 \times 2\). The laws of indices let us simplify + expressions efficiently and work with very large or very small quantities. +

+ +

Meaning of indices

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ExpressionMeaningValue
\(5^3\)\(5 \times 5 \times 5\)\(125\)
\(a^2\)\(a \times a\)depends on \(a\)
\(10^0\)zero index\(1\)
+
+ +

Core laws of indices

+
+

Index laws (for non-zero base where required)

+

+ \[ + a^m \cdot a^n = a^{m+n}, \quad + \frac{a^m}{a^n} = a^{m-n}, \quad + (a^m)^n = a^{mn} + \] + \[ + (ab)^n = a^n b^n, \quad + \left(\frac{a}{b}\right)^n = \frac{a^n}{b^n}, \quad + a^0 = 1 + \] +

+
+ +

Negative indices

+

+ A negative index means reciprocal: + \[ + a^{-n} = \frac{1}{a^n}, \qquad a \neq 0. + \] +

+

Example. \(2^{-3} = \frac{1}{2^3} = \frac{1}{8}\).

+

Example. \(x^{-2}y^3 = \frac{y^3}{x^2}\).

+ +

Fractional indices

+

+ Fractional indices represent roots: + \[ + a^{1/n} = \sqrt[n]{a}, \qquad + a^{m/n} = \sqrt[n]{a^m}. + \] +

+

Example. \(27^{1/3} = 3\).

+

Example. \(16^{3/4} = (\sqrt[4]{16})^3 = 2^3 = 8\).

+ +

Simplification examples

+

Example 1. Simplify \(a^5 \cdot a^{-2}\).

+

+ \(a^{5+(-2)} = a^3\). +

+

Example 2. Simplify \(\dfrac{3x^4y^{-1}}{6x^2}\).

+

+ \[ + \frac{3x^4y^{-1}}{6x^2} + = \frac{1}{2}x^{4-2}y^{-1} + = \frac{x^2}{2y}. + \] +

+

Example 3. Simplify \((2a^3b^{-2})^2\).

+

+ \(2^2 a^{6} b^{-4} = \dfrac{4a^6}{b^4}\). +

+ +

Scientific notation link

+

+ Indices are central to scientific notation: + \[ + N = k \times 10^n, \qquad 1 \leq k < 10. + \] + Positive \(n\) gives large numbers, negative \(n\) gives small decimals. +

+

Example. \(0.00045 = 4.5 \times 10^{-4}\).

+
+ +
+

History

+

+ Exponent ideas developed over centuries as mathematicians searched for compact ways to write + repeated multiplication. Ancient methods used words or special marks, but a systematic notation + appeared gradually in early modern Europe. +

+

+ In the 17th century, mathematicians such as Descartes helped popularize writing + powers with superscripts (like \(x^2\), \(x^3\)). Later work by + Wallis and Newton expanded these ideas to fractional and + negative exponents. This notation became a foundation for algebra, calculus, and scientific + notation. +

+ +

Derivation

+ +

Why \(a^0 = 1\)

+

+ Using the quotient law with \(a \neq 0\): + \[ + \frac{a^m}{a^m} = a^{m-m} = a^0. + \] + But \(\dfrac{a^m}{a^m} = 1\), so \(a^0 = 1\). +

+ +

Why \(a^{-n} = \dfrac{1}{a^n}\)

+

+ Again from the quotient law: + \[ + \frac{a^0}{a^n} = a^{0-n} = a^{-n}. + \] + Since \(a^0 = 1\), we get \(a^{-n} = \dfrac{1}{a^n}\). +

+ +

Why \(a^{1/n} = \sqrt[n]{a}\)

+

+ Let \(a^{1/n} = t\). Raise both sides to power \(n\): + \[ + (a^{1/n})^n = t^n \Rightarrow a = t^n. + \] + So \(t\) is an \(n\)-th root of \(a\), giving \(a^{1/n} = \sqrt[n]{a}\). +

+ +

Checkpoints

+

+ Pause and reason before continuing. Discuss with a classmate or write your reasoning in a + notebook. +

+ +
+

Checkpoint

+

+ Simplify \(x^7 \div x^3\). Which index law did you use? +

+
+ +
+

Checkpoint

+

+ Rewrite \(5^{-2}\) without a negative index, then evaluate. +

+
+ +
+

Checkpoint

+

+ Simplify \((a^2b^{-1})^3\) and express your final answer with positive indices. +

+
+ +
+

Checkpoint

+

+ Evaluate \(81^{3/4}\) by rewriting it in root form first. +

+
+ +
+

Checkpoint

+

+ Write \(0.000072\) in scientific notation and state the index on 10. +

+
+ +
+

Checkpoint

+

+ True or false: \((x+y)^2 = x^2 + y^2\). Explain why index laws do or do not support this. +

+
+ +

Applications

+ +

Large and small scales in science

+

+ Distances in astronomy and sizes of atoms are usually written with powers of 10 to keep numbers + readable. For example, \(1.5 \times 10^{11}\) m is much clearer than writing 150000000000 m. +

+ +

Repeated growth and decay

+

+ Compound growth often uses exponent models such as + \(A = P(1+r)^t\). Exponents track repeated multiplication over equal time steps. +

+ +

Computing and binary powers

+

+ Computer memory sizes are tied to powers of 2. For example, \(2^{10} = 1024\), so one kibibyte + is 1024 bytes and larger units build from further powers. +

+ +

References

+ +
diff --git a/content/math/laws-of-indices/core.html b/content/math/laws-of-indices/core.html new file mode 100644 index 0000000..68a0d76 --- /dev/null +++ b/content/math/laws-of-indices/core.html @@ -0,0 +1,103 @@ +
+

+ An index (or exponent) tells you how many times a base is used as a factor. + For example, \(2^4 = 2 \times 2 \times 2 \times 2\). The laws of indices let us simplify + expressions efficiently and work with very large or very small quantities. +

+ +

Meaning of indices

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
ExpressionMeaningValue
\(5^3\)\(5 \times 5 \times 5\)\(125\)
\(a^2\)\(a \times a\)depends on \(a\)
\(10^0\)zero index\(1\)
+
+ +

Core laws of indices

+
+

Index laws (for non-zero base where required)

+

+ \[ + a^m \cdot a^n = a^{m+n}, \quad + \frac{a^m}{a^n} = a^{m-n}, \quad + (a^m)^n = a^{mn} + \] + \[ + (ab)^n = a^n b^n, \quad + \left(\frac{a}{b}\right)^n = \frac{a^n}{b^n}, \quad + a^0 = 1 + \] +

+
+ +

Negative indices

+

+ A negative index means reciprocal: + \[ + a^{-n} = \frac{1}{a^n}, \qquad a \neq 0. + \] +

+

Example. \(2^{-3} = \frac{1}{2^3} = \frac{1}{8}\).

+

Example. \(x^{-2}y^3 = \frac{y^3}{x^2}\).

+ +

Fractional indices

+

+ Fractional indices represent roots: + \[ + a^{1/n} = \sqrt[n]{a}, \qquad + a^{m/n} = \sqrt[n]{a^m}. + \] +

+

Example. \(27^{1/3} = 3\).

+

Example. \(16^{3/4} = (\sqrt[4]{16})^3 = 2^3 = 8\).

+ +

Simplification examples

+

Example 1. Simplify \(a^5 \cdot a^{-2}\).

+

+ \(a^{5+(-2)} = a^3\). +

+

Example 2. Simplify \(\dfrac{3x^4y^{-1}}{6x^2}\).

+

+ \[ + \frac{3x^4y^{-1}}{6x^2} + = \frac{1}{2}x^{4-2}y^{-1} + = \frac{x^2}{2y}. + \] +

+

Example 3. Simplify \((2a^3b^{-2})^2\).

+

+ \(2^2 a^{6} b^{-4} = \dfrac{4a^6}{b^4}\). +

+ +

Scientific notation link

+

+ Indices are central to scientific notation: + \[ + N = k \times 10^n, \qquad 1 \leq k < 10. + \] + Positive \(n\) gives large numbers, negative \(n\) gives small decimals. +

+

Example. \(0.00045 = 4.5 \times 10^{-4}\).

+
diff --git a/content/math/laws-of-indices/supplement.html b/content/math/laws-of-indices/supplement.html new file mode 100644 index 0000000..657e607 --- /dev/null +++ b/content/math/laws-of-indices/supplement.html @@ -0,0 +1,119 @@ +
+

History

+

+ Exponent ideas developed over centuries as mathematicians searched for compact ways to write + repeated multiplication. Ancient methods used words or special marks, but a systematic notation + appeared gradually in early modern Europe. +

+

+ In the 17th century, mathematicians such as Descartes helped popularize writing + powers with superscripts (like \(x^2\), \(x^3\)). Later work by + Wallis and Newton expanded these ideas to fractional and + negative exponents. This notation became a foundation for algebra, calculus, and scientific + notation. +

+ +

Derivation

+ +

Why \(a^0 = 1\)

+

+ Using the quotient law with \(a \neq 0\): + \[ + \frac{a^m}{a^m} = a^{m-m} = a^0. + \] + But \(\dfrac{a^m}{a^m} = 1\), so \(a^0 = 1\). +

+ +

Why \(a^{-n} = \dfrac{1}{a^n}\)

+

+ Again from the quotient law: + \[ + \frac{a^0}{a^n} = a^{0-n} = a^{-n}. + \] + Since \(a^0 = 1\), we get \(a^{-n} = \dfrac{1}{a^n}\). +

+ +

Why \(a^{1/n} = \sqrt[n]{a}\)

+

+ Let \(a^{1/n} = t\). Raise both sides to power \(n\): + \[ + (a^{1/n})^n = t^n \Rightarrow a = t^n. + \] + So \(t\) is an \(n\)-th root of \(a\), giving \(a^{1/n} = \sqrt[n]{a}\). +

+ +

Checkpoints

+

+ Pause and reason before continuing. Discuss with a classmate or write your reasoning in a + notebook. +

+ +
+

Checkpoint

+

+ Simplify \(x^7 \div x^3\). Which index law did you use? +

+
+ +
+

Checkpoint

+

+ Rewrite \(5^{-2}\) without a negative index, then evaluate. +

+
+ +
+

Checkpoint

+

+ Simplify \((a^2b^{-1})^3\) and express your final answer with positive indices. +

+
+ +
+

Checkpoint

+

+ Evaluate \(81^{3/4}\) by rewriting it in root form first. +

+
+ +
+

Checkpoint

+

+ Write \(0.000072\) in scientific notation and state the index on 10. +

+
+ +
+

Checkpoint

+

+ True or false: \((x+y)^2 = x^2 + y^2\). Explain why index laws do or do not support this. +

+
+ +

Applications

+ +

Large and small scales in science

+

+ Distances in astronomy and sizes of atoms are usually written with powers of 10 to keep numbers + readable. For example, \(1.5 \times 10^{11}\) m is much clearer than writing 150000000000 m. +

+ +

Repeated growth and decay

+

+ Compound growth often uses exponent models such as + \(A = P(1+r)^t\). Exponents track repeated multiplication over equal time steps. +

+ +

Computing and binary powers

+

+ Computer memory sizes are tied to powers of 2. For example, \(2^{10} = 1024\), so one kibibyte + is 1024 bytes and larger units build from further powers. +

+ +

References

+ +
diff --git a/content/math/linear-inequalities-one-unknown/_index.md b/content/math/linear-inequalities-one-unknown/_index.md index 57dc924..8a0f259 100644 --- a/content/math/linear-inequalities-one-unknown/_index.md +++ b/content/math/linear-inequalities-one-unknown/_index.md @@ -8,3 +8,251 @@ subject = "math" chapter_id = "linear-inequalities-one-unknown" strand = "Number & Algebra" +++ + +
+

+ A linear inequality in one unknown compares a linear expression with a value or + another linear expression using symbols such as + \(<\), \(>\), \(\leq\), and \(\geq\). Instead of finding one exact value, we usually find a + set of values that make the statement true. +

+ +

Inequality symbols

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SymbolMeaningExample
\(<\)Strictly less than\(x < 4\)
\(>\)Strictly greater than\(x > -2\)
\(\leq\)Less than or equal to\(x \leq 7\)
\(\geq\)Greater than or equal to\(x \geq 0\)
+
+ +

Solving linear inequalities

+

+ Solve them much like linear equations: expand brackets when needed, collect like terms, then + isolate the variable. +

+

Example. Solve \(3x - 5 \leq 16\).

+

+ Add \(5\) to both sides: \(3x \leq 21\). Divide by \(3\): \(x \leq 7\). +

+ +

The sign-flip rule

+

+ When you multiply or divide both sides of an inequality by a negative number, the + inequality sign must reverse. +

+
+

Key rule

+

+ \[ + a < b \;\Rightarrow\; -a > -b + \] +

+
+

Example. Solve \(-2x + 1 > 9\).

+

+ Subtract \(1\): \(-2x > 8\). Divide by \(-2\), so reverse \(>\) to \(<\): + \(x < -4\). +

+ +

Number line and interval notation

+

+ The solution set can be written on a number line or in interval notation: +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
InequalityInterval notationEndpoint style
\(x < 3\)\((-\infty, 3)\)Open at \(3\)
\(x \leq 3\)\((-\infty, 3]\)Closed at \(3\)
\(x > -1\)\((-1, \infty)\)Open at \(-1\)
\(x \geq -1\)\([-1, \infty)\)Closed at \(-1\)
+
+ +

Compound inequalities

+

+ Compound inequalities combine two inequalities with and or or. +

+

Example (and). Solve \(2 < x + 1 \leq 6\).

+

+ Subtract \(1\) from all three parts: + \(1 < x \leq 5\). Interval form: \((1, 5]\). +

+

Example (or). Solve \(x - 4 < -7\) or \(3x + 1 \geq 10\).

+

+ First: \(x < -3\). Second: \(3x \geq 9\), so \(x \geq 3\). + Final set: \((-\infty, -3) \cup [3, \infty)\). +

+ +

Word problems

+

+ Translate the condition into an inequality, solve, and interpret the answer in context. +

+

Example. A bus ticket costs \$12. You have at most \$90. How many tickets \(n\) + can you buy?

+

+ \(12n \leq 90 \Rightarrow n \leq 7.5\). Since \(n\) is a whole number of tickets, the maximum is + \(7\). +

+
+ +
+

History

+

+ Ideas behind inequalities are ancient. Greek mathematicians such as Euclid + compared magnitudes and proved strict order relationships long before modern algebraic symbols. + In India and the Islamic world, later algebra texts worked with bounds and constraints in + practical problems involving trade, measurement, and inheritance. +

+

+ The modern symbols \(<\) and \(>\) were introduced in the 17th century by + Thomas Harriot. As algebra notation became standard, inequalities became a + compact way to describe ranges of possible values rather than one exact answer. + Today, the same language appears in budgeting, engineering safety margins, and optimization. +

+ +

Derivation

+ +

Why does the sign flip with a negative?

+

+ Suppose \(a < b\). Subtract \(b\) from both sides to get \(a-b < 0\). Multiplying by \(-1\) + gives \(b-a > 0\), so \(-a > -b\). Geometrically, multiplying by \(-1\) reflects numbers + across zero on the number line, reversing left-right order. That is why dividing by a negative + must reverse \(<\) to \(>\) (and \(\leq\) to \(\geq\)). +

+ +

Why intervals match inequality endpoints

+

+ A strict inequality such as \(x < c\) excludes \(c\), so interval notation uses an open bracket + \((\cdot)\). A non-strict inequality such as \(x \leq c\) includes \(c\), so interval notation + uses a closed bracket \([\cdot]\). This bracket choice encodes exactly the same endpoint logic + as open and closed dots on number lines. +

+ +

Checkpoints

+

+ Pause and reason before moving on. Discuss with a classmate or write your reasoning in a + notebook. +

+ +
+

Checkpoint

+

+ Solve \(4x - 3 < 9\). Write your answer as both an inequality and an interval. +

+
+ +
+

Checkpoint

+

+ Solve \(-5x \geq 20\). Which step forces a sign flip, and what is the final solution? +

+
+ +
+

Checkpoint

+

+ Is \(x = -2\) a solution of both \(x \leq -2\) and \(x < -2\)? Explain the difference. +

+
+ +
+

Checkpoint

+

+ Solve \(1 \leq 2x + 3 < 9\). Then write the solution in interval notation. +

+
+ +
+

Checkpoint

+

+ Write and solve an inequality for this condition: "A package mass \(m\) must be at least + 0.5 kg and less than 2 kg." +

+
+ +
+

Checkpoint

+

+ Solve and sketch on a number line: \(x-6 < -10\) or \(2x+1 \geq 7\). +

+
+ +

Applications

+ +

Budgeting and spending limits

+

+ If each notebook costs \$3 and you have at most \$24, the purchase count \(n\) satisfies + \(3n \leq 24\). Inequalities model spending caps in daily life, event planning, and inventory + decisions. +

+ +

Safety and engineering limits

+

+ Many systems run within allowable ranges, such as pressure \(P \leq P_{\max}\) or temperature + \(T_{\min} \leq T \leq T_{\max}\). Inequalities represent these safe operating regions directly. +

+ +

Grades and performance thresholds

+

+ A pass condition might be "score at least 50," written as \(s \geq 50\). Scholarship criteria + and qualification cutoffs are also inequality-based rules. +

+ +

References

+ +
diff --git a/content/math/linear-inequalities-one-unknown/core.html b/content/math/linear-inequalities-one-unknown/core.html new file mode 100644 index 0000000..4d69cc0 --- /dev/null +++ b/content/math/linear-inequalities-one-unknown/core.html @@ -0,0 +1,136 @@ +
+

+ A linear inequality in one unknown compares a linear expression with a value or + another linear expression using symbols such as + \(<\), \(>\), \(\leq\), and \(\geq\). Instead of finding one exact value, we usually find a + set of values that make the statement true. +

+ +

Inequality symbols

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SymbolMeaningExample
\(<\)Strictly less than\(x < 4\)
\(>\)Strictly greater than\(x > -2\)
\(\leq\)Less than or equal to\(x \leq 7\)
\(\geq\)Greater than or equal to\(x \geq 0\)
+
+ +

Solving linear inequalities

+

+ Solve them much like linear equations: expand brackets when needed, collect like terms, then + isolate the variable. +

+

Example. Solve \(3x - 5 \leq 16\).

+

+ Add \(5\) to both sides: \(3x \leq 21\). Divide by \(3\): \(x \leq 7\). +

+ +

The sign-flip rule

+

+ When you multiply or divide both sides of an inequality by a negative number, the + inequality sign must reverse. +

+
+

Key rule

+

+ \[ + a < b \;\Rightarrow\; -a > -b + \] +

+
+

Example. Solve \(-2x + 1 > 9\).

+

+ Subtract \(1\): \(-2x > 8\). Divide by \(-2\), so reverse \(>\) to \(<\): + \(x < -4\). +

+ +

Number line and interval notation

+

+ The solution set can be written on a number line or in interval notation: +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
InequalityInterval notationEndpoint style
\(x < 3\)\((-\infty, 3)\)Open at \(3\)
\(x \leq 3\)\((-\infty, 3]\)Closed at \(3\)
\(x > -1\)\((-1, \infty)\)Open at \(-1\)
\(x \geq -1\)\([-1, \infty)\)Closed at \(-1\)
+
+ +

Compound inequalities

+

+ Compound inequalities combine two inequalities with and or or. +

+

Example (and). Solve \(2 < x + 1 \leq 6\).

+

+ Subtract \(1\) from all three parts: + \(1 < x \leq 5\). Interval form: \((1, 5]\). +

+

Example (or). Solve \(x - 4 < -7\) or \(3x + 1 \geq 10\).

+

+ First: \(x < -3\). Second: \(3x \geq 9\), so \(x \geq 3\). + Final set: \((-\infty, -3) \cup [3, \infty)\). +

+ +

Word problems

+

+ Translate the condition into an inequality, solve, and interpret the answer in context. +

+

Example. A bus ticket costs \$12. You have at most \$90. How many tickets \(n\) + can you buy?

+

+ \(12n \leq 90 \Rightarrow n \leq 7.5\). Since \(n\) is a whole number of tickets, the maximum is + \(7\). +

+
diff --git a/content/math/linear-inequalities-one-unknown/supplement.html b/content/math/linear-inequalities-one-unknown/supplement.html new file mode 100644 index 0000000..7d943cc --- /dev/null +++ b/content/math/linear-inequalities-one-unknown/supplement.html @@ -0,0 +1,110 @@ +
+

History

+

+ Ideas behind inequalities are ancient. Greek mathematicians such as Euclid + compared magnitudes and proved strict order relationships long before modern algebraic symbols. + In India and the Islamic world, later algebra texts worked with bounds and constraints in + practical problems involving trade, measurement, and inheritance. +

+

+ The modern symbols \(<\) and \(>\) were introduced in the 17th century by + Thomas Harriot. As algebra notation became standard, inequalities became a + compact way to describe ranges of possible values rather than one exact answer. + Today, the same language appears in budgeting, engineering safety margins, and optimization. +

+ +

Derivation

+ +

Why does the sign flip with a negative?

+

+ Suppose \(a < b\). Subtract \(b\) from both sides to get \(a-b < 0\). Multiplying by \(-1\) + gives \(b-a > 0\), so \(-a > -b\). Geometrically, multiplying by \(-1\) reflects numbers + across zero on the number line, reversing left-right order. That is why dividing by a negative + must reverse \(<\) to \(>\) (and \(\leq\) to \(\geq\)). +

+ +

Why intervals match inequality endpoints

+

+ A strict inequality such as \(x < c\) excludes \(c\), so interval notation uses an open bracket + \((\cdot)\). A non-strict inequality such as \(x \leq c\) includes \(c\), so interval notation + uses a closed bracket \([\cdot]\). This bracket choice encodes exactly the same endpoint logic + as open and closed dots on number lines. +

+ +

Checkpoints

+

+ Pause and reason before moving on. Discuss with a classmate or write your reasoning in a + notebook. +

+ +
+

Checkpoint

+

+ Solve \(4x - 3 < 9\). Write your answer as both an inequality and an interval. +

+
+ +
+

Checkpoint

+

+ Solve \(-5x \geq 20\). Which step forces a sign flip, and what is the final solution? +

+
+ +
+

Checkpoint

+

+ Is \(x = -2\) a solution of both \(x \leq -2\) and \(x < -2\)? Explain the difference. +

+
+ +
+

Checkpoint

+

+ Solve \(1 \leq 2x + 3 < 9\). Then write the solution in interval notation. +

+
+ +
+

Checkpoint

+

+ Write and solve an inequality for this condition: "A package mass \(m\) must be at least + 0.5 kg and less than 2 kg." +

+
+ +
+

Checkpoint

+

+ Solve and sketch on a number line: \(x-6 < -10\) or \(2x+1 \geq 7\). +

+
+ +

Applications

+ +

Budgeting and spending limits

+

+ If each notebook costs \$3 and you have at most \$24, the purchase count \(n\) satisfies + \(3n \leq 24\). Inequalities model spending caps in daily life, event planning, and inventory + decisions. +

+ +

Safety and engineering limits

+

+ Many systems run within allowable ranges, such as pressure \(P \leq P_{\max}\) or temperature + \(T_{\min} \leq T \leq T_{\max}\). Inequalities represent these safe operating regions directly. +

+ +

Grades and performance thresholds

+

+ A pass condition might be "score at least 50," written as \(s \geq 50\). Scholarship criteria + and qualification cutoffs are also inequality-based rules. +

+ +

References

+ +
diff --git a/data/contributors.json b/data/contributors.json index 7c0eec1..7559673 100644 --- a/data/contributors.json +++ b/data/contributors.json @@ -7,10 +7,9 @@ "job_title": "EdTech specialist / software engineer", "education": "BEng in Computer Science (HKUST) + BBA (HKUST)", "contributions": [ - "Founder", + "Project Creator", "Math textbook", - "Physics textbook", - "Chemistry textbook" + "Physics textbook" ], "socials": [ { "type": "email", "url": "manlung.yeung@outlook.com" }, diff --git a/data/math-curriculum.json b/data/math-curriculum.json index b8ba26c..2756df1 100644 --- a/data/math-curriculum.json +++ b/data/math-curriculum.json @@ -466,7 +466,7 @@ "slug": "linear-inequalities-one-unknown", "title": "Linear inequalities in one unknown", "description": "Solve and represent linear inequalities on the number line and in word problems.", - "status": "planned", + "status": "live", "curriculumCoverage": { "DSE": "core", "IGCSE": "core", @@ -483,7 +483,7 @@ "slug": "laws-of-indices", "title": "Laws of indices", "description": "Apply index laws to simplify expressions with integer and rational exponents.", - "status": "planned", + "status": "live", "curriculumCoverage": { "DSE": "core", "IGCSE": "core", diff --git a/package.json b/package.json index 964df5f..3f19335 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,9 @@ "type": "module", "scripts": { "sync:chapters": "bun scripts/sync-chapters.ts", + "generate:site-metadata": "bun scripts/generate-site-metadata.ts", "build:js": "bun run sync:chapters && vite build", - "build": "bun run check:static-html && bun run validate:curriculum && rm -f themes/openfreebooks/static/main.css public/main.css && bun run build:js && zola build && bun run index:search", + "build": "bun run check:static-html && bun run validate:curriculum && rm -f themes/openfreebooks/static/main.css public/main.css && bun run build:js && bun run generate:site-metadata && zola build && bun run index:search", "index:search": "pagefind --site public --glob \"**/index.html\" && rm -rf static/pagefind && cp -R public/pagefind static/pagefind", "dev:js": "bun run sync:chapters && vite build --watch", "dev": "bash scripts/dev.sh", diff --git a/scripts/generate-site-metadata.ts b/scripts/generate-site-metadata.ts new file mode 100644 index 0000000..e8700bb --- /dev/null +++ b/scripts/generate-site-metadata.ts @@ -0,0 +1,322 @@ +import { existsSync } from "node:fs"; +import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises"; +import { dirname, join, relative, sep } from "node:path"; +import { $ } from "bun"; + +type FrontMatter = { + title?: string; + description?: string; + template?: string; +}; + +type CurriculumChapter = { + slug?: unknown; + status?: unknown; + title?: unknown; + description?: unknown; +}; + +type CurriculumStrand = { + chapters?: unknown; +}; + +type CurriculumFile = { + strands?: unknown; +}; + +type UrlEntry = { + urlPath: string; + title: string; + description: string; + sourcePaths: string[]; + lastmod: Date | null; +}; + +const ROOT = join(import.meta.dir, ".."); +const CONTENT_DIR = join(ROOT, "content"); +const DATA_DIR = join(ROOT, "data"); +const STATIC_DIR = join(ROOT, "static"); +const SITEMAP_PATH = join(STATIC_DIR, "sitemap.xml"); +const FEED_PATH = join(STATIC_DIR, "feed.xml"); + +function xmlEscape(value: string): string { + return value + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function isNonEmptyString(value: unknown): value is string { + return typeof value === "string" && value.trim().length > 0; +} + +function toPosixPath(path: string): string { + return path.split(sep).join("/"); +} + +function normalizeBaseUrl(rawBaseUrl: string): string { + return rawBaseUrl.replace(/\/+$/, ""); +} + +function ensureTrailingSlash(path: string): string { + return path.endsWith("/") ? path : `${path}/`; +} + +function parseFrontMatter(markdown: string): FrontMatter { + const match = markdown.match(/^\+\+\+([\s\S]*?)\+\+\+/); + if (!match) return {}; + const block = match[1]; + + const readKey = (key: string): string | undefined => { + const lineMatch = block.match(new RegExp(`^\\s*${key}\\s*=\\s*"([^"]*)"\\s*$`, "m")); + return lineMatch?.[1]?.trim(); + }; + + return { + title: readKey("title"), + description: readKey("description"), + template: readKey("template"), + }; +} + +async function readBaseUrlFromConfig(): Promise { + const zolaPath = join(ROOT, "zola.toml"); + const raw = await readFile(zolaPath, "utf8"); + const match = raw.match(/^\s*base_url\s*=\s*"([^"]+)"\s*$/m); + if (!match) { + throw new Error("Could not read base_url from zola.toml"); + } + return normalizeBaseUrl(match[1]); +} + +async function discoverMarkdownFiles(dir: string): Promise { + const output: string[] = []; + const entries = await readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.name.startsWith(".")) continue; + const abs = join(dir, entry.name); + if (entry.isDirectory()) { + output.push(...(await discoverMarkdownFiles(abs))); + continue; + } + if (entry.isFile() && entry.name.endsWith(".md")) { + output.push(abs); + } + } + return output; +} + +async function readLiveChapterKeys(): Promise> { + const keys = new Set(); + const entries = await readdir(DATA_DIR, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isFile() || !entry.name.endsWith("-curriculum.json")) continue; + const subject = entry.name.replace(/-curriculum\.json$/, ""); + const jsonPath = join(DATA_DIR, entry.name); + const raw = await readFile(jsonPath, "utf8"); + const data = JSON.parse(raw) as CurriculumFile; + if (!Array.isArray(data.strands)) continue; + for (const strand of data.strands as CurriculumStrand[]) { + if (!Array.isArray(strand.chapters)) continue; + for (const chapter of strand.chapters as CurriculumChapter[]) { + if (!isNonEmptyString(chapter.slug)) continue; + if (chapter.status !== "live") continue; + keys.add(`${subject}/${chapter.slug}`); + } + } + } + return keys; +} + +async function readGitLastmod(sourcePaths: string[]): Promise { + const relPaths = sourcePaths + .map((sourcePath) => toPosixPath(relative(ROOT, sourcePath))) + .filter((rel) => rel.length > 0); + + if (relPaths.length === 0) return null; + const proc = await $`git --no-pager log -1 --format=%cI -- ${relPaths}`.quiet(); + if (proc.exitCode !== 0) return null; + + const raw = proc.stdout.toString().trim(); + if (!raw) return null; + + const parsed = new Date(raw); + return Number.isNaN(parsed.getTime()) ? null : parsed; +} + +async function readFilesystemLastmod(sourcePaths: string[]): Promise { + let latestMs = 0; + + const visit = async (path: string): Promise => { + const info = await stat(path); + if (info.isDirectory()) { + const entries = await readdir(path, { withFileTypes: true }); + for (const entry of entries) { + if (entry.name.startsWith(".")) continue; + await visit(join(path, entry.name)); + } + return; + } + if (info.mtimeMs > latestMs) { + latestMs = info.mtimeMs; + } + }; + + for (const sourcePath of sourcePaths) { + if (!existsSync(sourcePath)) continue; + await visit(sourcePath); + } + + if (!latestMs) return null; + return new Date(latestMs); +} + +function toUrlPathFromMarkdown(contentRelativePath: string): string { + const rel = toPosixPath(contentRelativePath); + if (rel === "_index.md") return "/"; + if (rel.endsWith("/_index.md")) { + return ensureTrailingSlash(`/${rel.replace(/\/_index\.md$/, "")}`); + } + return ensureTrailingSlash(`/${rel.replace(/\.md$/, "")}`); +} + +function isChapterIndex(contentRelativePath: string, fm: FrontMatter): boolean { + return contentRelativePath.endsWith("/_index.md") && fm.template === "chapter.html"; +} + +function chapterKeyFromContentPath(contentRelativePath: string): string | null { + const parts = toPosixPath(contentRelativePath).split("/"); + if (parts.length !== 3 || parts[2] !== "_index.md") return null; + return `${parts[0]}/${parts[1]}`; +} + +async function buildEntries(): Promise { + const markdownFiles = await discoverMarkdownFiles(CONTENT_DIR); + const liveChapterKeys = await readLiveChapterKeys(); + const entries: UrlEntry[] = []; + + for (const absMarkdownPath of markdownFiles) { + const relPath = toPosixPath(relative(CONTENT_DIR, absMarkdownPath)); + const raw = await readFile(absMarkdownPath, "utf8"); + const fm = parseFrontMatter(raw); + + if (isChapterIndex(relPath, fm)) { + const key = chapterKeyFromContentPath(relPath); + if (!key || !liveChapterKeys.has(key)) continue; + const chapterDir = join(CONTENT_DIR, key); + const sourcePaths = [chapterDir]; + const lastmod = (await readGitLastmod(sourcePaths)) ?? (await readFilesystemLastmod(sourcePaths)); + entries.push({ + urlPath: toUrlPathFromMarkdown(relPath), + title: fm.title ?? key.split("/")[1], + description: fm.description ?? fm.title ?? key.split("/")[1], + sourcePaths, + lastmod, + }); + continue; + } + + const urlPath = toUrlPathFromMarkdown(relPath); + if (urlPath === "/404/") continue; + + const sourcePaths = [absMarkdownPath]; + const lastmod = (await readGitLastmod(sourcePaths)) ?? (await readFilesystemLastmod(sourcePaths)); + const fallbackTitle = + urlPath === "/" + ? "Open Free Books" + : urlPath + .split("/") + .filter(Boolean) + .at(-1) + ?.replaceAll("-", " ") ?? "Page"; + + entries.push({ + urlPath, + title: fm.title ?? fallbackTitle, + description: fm.description ?? fm.title ?? fallbackTitle, + sourcePaths, + lastmod, + }); + } + + const dedup = new Map(); + for (const entry of entries) { + if (!dedup.has(entry.urlPath)) { + dedup.set(entry.urlPath, entry); + } + } + + const result = [...dedup.values()]; + result.sort((a, b) => a.urlPath.localeCompare(b.urlPath)); + return result; +} + +function renderSitemap(baseUrl: string, entries: UrlEntry[]): string { + const lines = ['', '']; + + for (const entry of entries) { + const loc = `${baseUrl}${entry.urlPath}`; + lines.push(" "); + lines.push(` ${xmlEscape(loc)}`); + if (entry.lastmod) { + lines.push(` ${entry.lastmod.toISOString()}`); + } + lines.push(" "); + } + + lines.push("", ""); + return lines.join("\n"); +} + +function renderFeed(baseUrl: string, entries: UrlEntry[]): string { + const sorted = [...entries].sort((a, b) => { + const aMs = a.lastmod?.getTime() ?? 0; + const bMs = b.lastmod?.getTime() ?? 0; + return bMs - aMs; + }); + const latest = sorted.find((entry) => entry.lastmod)?.lastmod ?? new Date(); + + const lines = [ + '', + '', + " ", + " Open Free Books", + ` ${xmlEscape(`${baseUrl}/`)}`, + " Recent updates to Open Free Books pages and live chapters.", + ` ${latest.toUTCString()}`, + ]; + + for (const entry of sorted) { + const link = `${baseUrl}${entry.urlPath}`; + const pubDate = (entry.lastmod ?? latest).toUTCString(); + lines.push(" "); + lines.push(` ${xmlEscape(entry.title)}`); + lines.push(` ${xmlEscape(link)}`); + lines.push(` ${xmlEscape(link)}`); + lines.push(` ${xmlEscape(pubDate)}`); + lines.push(` ${xmlEscape(entry.description)}`); + lines.push(" "); + } + + lines.push(" ", "", ""); + return lines.join("\n"); +} + +async function main() { + const baseUrl = await readBaseUrlFromConfig(); + const entries = await buildEntries(); + await mkdir(dirname(SITEMAP_PATH), { recursive: true }); + await writeFile(SITEMAP_PATH, renderSitemap(baseUrl, entries)); + await writeFile(FEED_PATH, renderFeed(baseUrl, entries)); + + if (!existsSync(SITEMAP_PATH) || !existsSync(FEED_PATH)) { + throw new Error("Failed to generate sitemap/feed outputs"); + } + + console.log(`Generated ${toPosixPath(relative(ROOT, SITEMAP_PATH))} with ${entries.length} URL(s).`); + console.log(`Generated ${toPosixPath(relative(ROOT, FEED_PATH))} with ${entries.length} item(s).`); +} + +main(); diff --git a/static/feed.xml b/static/feed.xml new file mode 100644 index 0000000..040accf --- /dev/null +++ b/static/feed.xml @@ -0,0 +1,177 @@ + + + + Open Free Books + https://openfreebooks.org/ + Recent updates to Open Free Books pages and live chapters. + Thu, 21 May 2026 06:54:29 GMT + + Laws of indices + https://openfreebooks.org/math/laws-of-indices/ + https://openfreebooks.org/math/laws-of-indices/ + Thu, 21 May 2026 06:54:29 GMT + Apply index laws to simplify expressions with integer and rational exponents. + + + Linear inequalities in one unknown + https://openfreebooks.org/math/linear-inequalities-one-unknown/ + https://openfreebooks.org/math/linear-inequalities-one-unknown/ + Thu, 21 May 2026 06:33:43 GMT + Solve, represent, and apply linear inequalities in one unknown using number lines and interval notation. + + + Counting and numbers + https://openfreebooks.org/math/counting-and-numbers/ + https://openfreebooks.org/math/counting-and-numbers/ + Thu, 21 May 2026 05:05:38 GMT + Count objects, spot patterns, add, multiply, and divide; then meet place value and big numbers from hundreds to billions. + + + Linear equations in two unknowns + https://openfreebooks.org/math/linear-equations-two-unknowns/ + https://openfreebooks.org/math/linear-equations-two-unknowns/ + Thu, 21 May 2026 05:05:38 GMT + Solve pairs of linear equations by substitution, elimination, and graphs; classify unique, parallel, and coincident cases. + + + Written calculation + https://openfreebooks.org/math/written-calculation/ + https://openfreebooks.org/math/written-calculation/ + Thu, 21 May 2026 05:05:38 GMT + Add, subtract, multiply, and divide whole numbers using vertical written form: align columns, regroup, and read the result. + + + English + https://openfreebooks.org/english/ + https://openfreebooks.org/english/ + Thu, 21 May 2026 02:10:55 GMT + Browse English chapters in the curriculum catalog. + + + Reading comprehension foundations + https://openfreebooks.org/english/reading-comprehension-foundations/ + https://openfreebooks.org/english/reading-comprehension-foundations/ + Thu, 21 May 2026 02:10:55 GMT + Find the main idea, key details, and evidence in short texts. + + + FAQ + https://openfreebooks.org/faq/ + https://openfreebooks.org/faq/ + Thu, 21 May 2026 02:10:55 GMT + FAQ + + + Physics + https://openfreebooks.org/physics/ + https://openfreebooks.org/physics/ + Thu, 21 May 2026 02:10:55 GMT + Browse physics chapters in the curriculum catalog. + + + Force and Newton's laws + https://openfreebooks.org/physics/force-newtons-laws/ + https://openfreebooks.org/physics/force-newtons-laws/ + Thu, 21 May 2026 02:10:55 GMT + Use free-body diagrams, F = ma, and motion graphs to predict how objects move. + + + Basic math notation + https://openfreebooks.org/math/basic-math-notation/ + https://openfreebooks.org/math/basic-math-notation/ + Thu, 21 May 2026 01:07:39 GMT + Learn the names and meanings of plus, minus, times, divide, equals, brackets, and other signs used in number sentences. + + + Zero and one + https://openfreebooks.org/math/zero-and-one/ + https://openfreebooks.org/math/zero-and-one/ + Thu, 21 May 2026 01:07:39 GMT + 0 is nothing; 1 is one. Learn to tell them apart — the same skill as telling false from true — before counting larger amounts. + + + Algebraic fractions & formulas + https://openfreebooks.org/math/algebraic-fractions-formulas/ + https://openfreebooks.org/math/algebraic-fractions-formulas/ + Wed, 20 May 2026 20:17:25 GMT + Simplify and combine algebraic fractions; rearrange formulas in science contexts. + + + Directed numbers, rates & ratios + https://openfreebooks.org/math/directed-numbers-rates/ + https://openfreebooks.org/math/directed-numbers-rates/ + Wed, 20 May 2026 20:17:25 GMT + Operate with negative numbers and solve ratio, rate, and proportion problems in context. + + + Polynomials: expansion & factorisation + https://openfreebooks.org/math/polynomials-expansion-factorisation/ + https://openfreebooks.org/math/polynomials-expansion-factorisation/ + Wed, 20 May 2026 20:17:25 GMT + Expand products, collect like terms, and factorise expressions using common factors and identities. + + + Properties of operations + https://openfreebooks.org/math/properties-of-operations/ + https://openfreebooks.org/math/properties-of-operations/ + Wed, 20 May 2026 20:17:25 GMT + See why order, grouping, and distribution can change how you write a sum or product without changing the result — with interactive area and dot models. + + + Quadratic equations + https://openfreebooks.org/math/quadratic-equations/ + https://openfreebooks.org/math/quadratic-equations/ + Wed, 20 May 2026 20:17:25 GMT + The discriminant, nature of roots, and the graph of y = ax² + bx + c. + + + Search + https://openfreebooks.org/search/ + https://openfreebooks.org/search/ + Tue, 19 May 2026 19:29:49 GMT + Search + + + Contribute + https://openfreebooks.org/contributing/ + https://openfreebooks.org/contributing/ + Tue, 19 May 2026 12:34:25 GMT + Contribute + + + Mathematics + https://openfreebooks.org/math/ + https://openfreebooks.org/math/ + Tue, 19 May 2026 10:55:50 GMT + Browse mathematics chapters in the curriculum catalog. + + + Catalog + https://openfreebooks.org/catalog/ + https://openfreebooks.org/catalog/ + Tue, 19 May 2026 10:40:53 GMT + Catalog + + + Manifesto + https://openfreebooks.org/about/ + https://openfreebooks.org/about/ + Tue, 19 May 2026 06:35:42 GMT + Manifesto + + + Credits + https://openfreebooks.org/credits/ + https://openfreebooks.org/credits/ + Tue, 19 May 2026 05:33:20 GMT + Credits + + + Open Free Books + https://openfreebooks.org/ + https://openfreebooks.org/ + Tue, 19 May 2026 04:06:12 GMT + Free, open-source textbooks for every learner — elementary through university. Read the full book online at no cost, or contribute on GitHub. + + + diff --git a/static/sitemap.xml b/static/sitemap.xml new file mode 100644 index 0000000..ed80bde --- /dev/null +++ b/static/sitemap.xml @@ -0,0 +1,99 @@ + + + + https://openfreebooks.org/ + 2026-05-19T04:06:12.000Z + + + https://openfreebooks.org/about/ + 2026-05-19T06:35:42.000Z + + + https://openfreebooks.org/catalog/ + 2026-05-19T10:40:53.000Z + + + https://openfreebooks.org/contributing/ + 2026-05-19T12:34:25.000Z + + + https://openfreebooks.org/credits/ + 2026-05-19T05:33:20.000Z + + + https://openfreebooks.org/english/ + 2026-05-21T02:10:55.000Z + + + https://openfreebooks.org/english/reading-comprehension-foundations/ + 2026-05-21T02:10:55.000Z + + + https://openfreebooks.org/faq/ + 2026-05-21T02:10:55.000Z + + + https://openfreebooks.org/math/ + 2026-05-19T10:55:50.000Z + + + https://openfreebooks.org/math/algebraic-fractions-formulas/ + 2026-05-20T20:17:25.000Z + + + https://openfreebooks.org/math/basic-math-notation/ + 2026-05-21T01:07:39.000Z + + + https://openfreebooks.org/math/counting-and-numbers/ + 2026-05-21T05:05:38.000Z + + + https://openfreebooks.org/math/directed-numbers-rates/ + 2026-05-20T20:17:25.000Z + + + https://openfreebooks.org/math/laws-of-indices/ + 2026-05-21T06:54:29.553Z + + + https://openfreebooks.org/math/linear-equations-two-unknowns/ + 2026-05-21T05:05:38.000Z + + + https://openfreebooks.org/math/linear-inequalities-one-unknown/ + 2026-05-21T06:33:43.000Z + + + https://openfreebooks.org/math/polynomials-expansion-factorisation/ + 2026-05-20T20:17:25.000Z + + + https://openfreebooks.org/math/properties-of-operations/ + 2026-05-20T20:17:25.000Z + + + https://openfreebooks.org/math/quadratic-equations/ + 2026-05-20T20:17:25.000Z + + + https://openfreebooks.org/math/written-calculation/ + 2026-05-21T05:05:38.000Z + + + https://openfreebooks.org/math/zero-and-one/ + 2026-05-21T01:07:39.000Z + + + https://openfreebooks.org/physics/ + 2026-05-21T02:10:55.000Z + + + https://openfreebooks.org/physics/force-newtons-laws/ + 2026-05-21T02:10:55.000Z + + + https://openfreebooks.org/search/ + 2026-05-19T19:29:49.000Z + +