From 353b129533641a7776b855d038f787d6978fea65 Mon Sep 17 00:00:00 2001 From: Hananoshika Yomaru Date: Wed, 20 May 2026 23:54:57 -0700 Subject: [PATCH] Update build process and enhance curriculum content: Add a new script for generating site metadata to improve SEO with automatic sitemap and feed generation. Modify the build command to include this new script, ensuring it runs before the Zola build. Update the math curriculum to reflect the status change of the "Linear inequalities in one unknown" chapter to live, and enhance the chapter content with detailed explanations and examples. Adjust contributor roles in the JSON data for clarity. --- .agents/skills/ofb/SKILL.md | 11 + content/math/laws-of-indices/_index.md | 234 +++++++++++++ content/math/laws-of-indices/core.html | 103 ++++++ content/math/laws-of-indices/supplement.html | 119 +++++++ .../linear-inequalities-one-unknown/_index.md | 248 ++++++++++++++ .../linear-inequalities-one-unknown/core.html | 136 ++++++++ .../supplement.html | 110 ++++++ data/contributors.json | 5 +- data/math-curriculum.json | 4 +- package.json | 3 +- scripts/generate-site-metadata.ts | 322 ++++++++++++++++++ static/feed.xml | 177 ++++++++++ static/sitemap.xml | 99 ++++++ 13 files changed, 1565 insertions(+), 6 deletions(-) create mode 100644 content/math/laws-of-indices/_index.md create mode 100644 content/math/laws-of-indices/core.html create mode 100644 content/math/laws-of-indices/supplement.html create mode 100644 content/math/linear-inequalities-one-unknown/core.html create mode 100644 content/math/linear-inequalities-one-unknown/supplement.html create mode 100644 scripts/generate-site-metadata.ts create mode 100644 static/feed.xml create mode 100644 static/sitemap.xml 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 + +