diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..bb651af --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: HananoshikaYomaru diff --git a/content/contributing.md b/content/contributing.md index 47aae08..29fe610 100644 --- a/content/contributing.md +++ b/content/contributing.md @@ -2,3 +2,6 @@ title = "Contribute" template = "contributing.html" +++ + +Open Free Books is sustained by contributors and sponsors. +Thank you to everyone supporting this mission through [GitHub Sponsors](https://github.com/sponsors/HananoshikaYomaru). diff --git a/data/faq.json b/data/faq.json index eb3c7b6..8948d2f 100644 --- a/data/faq.json +++ b/data/faq.json @@ -15,7 +15,7 @@ "id": "how-different", "type": "comparison", "question": "How is Open Free Books different from LibreTexts, Open Textbook Library, Project Gutenberg, and OpenStax?", - "intro": "We respect these projects — they solve different problems. On many platforms, you find lots of different books for a single subject. With Open Free Books, there's just one textbook per subject—designed to be comprehensive so people everywhere can use it for any curriculum. That way, you don't need to pick between competing choices—you can just focus on learning. Oh, so this platform usually only provides static HTML without any interaction or just a PDF, but in OpenFreeBooks we have interactive demos An alleged map that are tailor made to visualize complex idea in simple ways.", + "intro": "We respect these projects — they solve different problems. On many platforms, you find lots of different books for a single subject. With Open Free Books, there's just one textbook per subject—designed to be comprehensive so people everywhere can use it for any curriculum. That way, you don't need to pick between competing choices—you can just focus on learning. Also these platforms usually only provide static HTML without any interaction or just PDFs, but in OpenFreeBooks we have interactive demos and knowledge maps that are tailor made to visualize complex idea in simple ways.", "peers": [ { "name": "LibreTexts", diff --git a/data/github-sponsors.json b/data/github-sponsors.json new file mode 100644 index 0000000..bdbe449 --- /dev/null +++ b/data/github-sponsors.json @@ -0,0 +1,20 @@ +{ + "title": "GitHub Sponsors", + "intro": "Supporters who help keep Open Free Books free and open for learners everywhere.", + "profile_url": "https://github.com/sponsors/HananoshikaYomaru", + "fetched_at": "2026-05-21T18:06:17.197Z", + "sponsors": [ + { + "login": "cybersader", + "name": "cybersader", + "avatar": "https://avatars.githubusercontent.com/u/106132469?u=12a0dcd73451d3369c414222b2a3b052938aa0e8&v=4", + "url": "https://github.com/cybersader" + }, + { + "login": "judej", + "name": "judej", + "avatar": "https://avatars.githubusercontent.com/u/547753?u=26add01b0f15c3a119bb78b37107567349620a34&v=4", + "url": "https://github.com/judej" + } + ] +} diff --git a/package.json b/package.json index 255fb71..f42b142 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "sync:chapters": "bun scripts/sync-chapters.ts", + "sync:sponsors": "bun scripts/fetch-github-sponsors.ts", "generate:site-metadata": "bun scripts/generate-site-metadata.ts", "build:js": "bun run sync:chapters && vite build", "build:site": "bun run generate:site-metadata && zola build && bun run index:search", diff --git a/scripts/fetch-github-sponsors.ts b/scripts/fetch-github-sponsors.ts new file mode 100644 index 0000000..b538630 --- /dev/null +++ b/scripts/fetch-github-sponsors.ts @@ -0,0 +1,145 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; + +type GraphQLError = { + message?: string; +}; + +type SponsorNode = { + sponsorEntity?: { + login?: string; + avatarUrl?: string; + url?: string; + } | null; +}; + +type SponsorsConnection = { + pageInfo?: { + hasNextPage?: boolean; + endCursor?: string | null; + }; + nodes?: SponsorNode[]; +}; + +type GraphQLResponse = { + data?: { + user?: { + sponsorshipsAsMaintainer?: SponsorsConnection; + } | null; + }; + errors?: GraphQLError[]; +}; + +type SponsorRecord = { + login: string; + name: string; + avatar: string; + url: string; +}; + +const ROOT = join(import.meta.dir, ".."); +const OUTPUT_PATH = join(ROOT, "data", "github-sponsors.json"); +const SPONSORABLE_LOGIN = "HananoshikaYomaru"; + +async function fetchSponsorPage(token: string, after: string | null): Promise { + const query = ` + query SponsorsAsMaintainer($login: String!, $after: String) { + user(login: $login) { + sponsorshipsAsMaintainer(first: 100, after: $after, includePrivate: false) { + pageInfo { + hasNextPage + endCursor + } + nodes { + sponsorEntity { + ... on User { + login + avatarUrl + url + } + } + } + } + } + } + `; + + const response = await fetch("https://api.github.com/graphql", { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + "User-Agent": "openfreebooks-sponsor-sync", + }, + body: JSON.stringify({ + query, + variables: { + login: SPONSORABLE_LOGIN, + after, + }, + }), + }); + + if (!response.ok) { + throw new Error(`GitHub GraphQL request failed (${response.status} ${response.statusText})`); + } + + const payload = (await response.json()) as GraphQLResponse; + if (payload.errors?.length) { + const message = payload.errors.map((err) => err.message).filter(Boolean).join("; "); + throw new Error(`GitHub GraphQL returned errors: ${message}`); + } + + const connection = payload.data?.user?.sponsorshipsAsMaintainer; + return connection ?? { nodes: [], pageInfo: { hasNextPage: false, endCursor: null } }; +} + +function toSponsorRecord(node: SponsorNode): SponsorRecord | null { + const entity = node.sponsorEntity; + if (!entity?.login || !entity.avatarUrl || !entity.url) return null; + + return { + login: entity.login, + name: entity.login, + avatar: entity.avatarUrl, + url: entity.url, + }; +} + +async function main() { + const token = process.env.GITHUB_TOKEN?.trim(); + if (!token) { + throw new Error("GITHUB_TOKEN is required to fetch GitHub Sponsors."); + } + + const sponsors: SponsorRecord[] = []; + let after: string | null = null; + + while (true) { + const connection = await fetchSponsorPage(token, after); + const nodes = connection.nodes ?? []; + for (const node of nodes) { + const sponsor = toSponsorRecord(node); + if (sponsor) sponsors.push(sponsor); + } + if (!connection.pageInfo?.hasNextPage) break; + after = connection.pageInfo.endCursor ?? null; + if (!after) break; + } + + sponsors.sort((a, b) => a.name.localeCompare(b.name, "en")); + + const output = { + title: "GitHub Sponsors", + intro: "Supporters who help keep Open Free Books free and open for learners everywhere.", + profile_url: `https://github.com/sponsors/${SPONSORABLE_LOGIN}`, + fetched_at: new Date().toISOString(), + sponsors, + }; + + await mkdir(dirname(OUTPUT_PATH), { recursive: true }); + await writeFile(OUTPUT_PATH, `${JSON.stringify(output, null, 2)}\n`, "utf8"); + console.log(`Wrote ${OUTPUT_PATH} with ${sponsors.length} sponsor(s).`); +} + +void main(); diff --git a/static/feed.xml b/static/feed.xml index a3bc0f9..19e5f96 100644 --- a/static/feed.xml +++ b/static/feed.xml @@ -4,40 +4,40 @@ Open Free Books https://openfreebooks.org/ Recent updates to Open Free Books pages and live chapters. - Thu, 21 May 2026 17:43:39 GMT - - Integer foundations - https://openfreebooks.org/math/integer-foundations/ - https://openfreebooks.org/math/integer-foundations/ - Thu, 21 May 2026 17:43:39 GMT - Model directed magnitude on number lines; use the zero principle and tiles to add and subtract integers, and see subtraction as adding an opposite. - + Thu, 21 May 2026 17:54:08 GMT Integer arithmetic https://openfreebooks.org/math/integer-arithmetic/ https://openfreebooks.org/math/integer-arithmetic/ - Thu, 21 May 2026 17:43:39 GMT + Thu, 21 May 2026 17:54:08 GMT Multiply and divide integers from models to symbolic rules; apply order of operations with signed numbers in multi-step expressions. + + Integer foundations + https://openfreebooks.org/math/integer-foundations/ + https://openfreebooks.org/math/integer-foundations/ + Thu, 21 May 2026 17:54:08 GMT + Model directed magnitude on number lines; use the zero principle and tiles to add and subtract integers, and see subtraction as adding an opposite. + Percentage applications https://openfreebooks.org/math/percentage-applications/ https://openfreebooks.org/math/percentage-applications/ - Thu, 21 May 2026 17:43:39 GMT + Thu, 21 May 2026 17:54:08 GMT Relate fractions and decimals to base-100 ratios; calculate percent increase and decrease in financial and real-world contexts. Fractions and decimals https://openfreebooks.org/math/rational-number-operations/ https://openfreebooks.org/math/rational-number-operations/ - Thu, 21 May 2026 17:43:39 GMT + Thu, 21 May 2026 17:54:08 GMT Convert between fractions, terminating decimals, and repeating decimals; add, subtract, multiply, and divide rational numbers and mixed numbers. Ratios & rates foundations https://openfreebooks.org/math/ratios-and-rates-foundation/ https://openfreebooks.org/math/ratios-and-rates-foundation/ - Thu, 21 May 2026 17:43:39 GMT + Thu, 21 May 2026 17:54:08 GMT Formulate part-to-part and part-to-whole ratios, find equivalent ratios, compute unit rates, and graph proportional relationships. diff --git a/static/sitemap.xml b/static/sitemap.xml index af68b9b..9b0073d 100644 --- a/static/sitemap.xml +++ b/static/sitemap.xml @@ -86,11 +86,11 @@ https://openfreebooks.org/math/integer-arithmetic/ - 2026-05-21T17:43:39.596Z + 2026-05-21T17:54:08.000Z https://openfreebooks.org/math/integer-foundations/ - 2026-05-21T17:43:39.989Z + 2026-05-21T17:54:08.000Z https://openfreebooks.org/math/introduction-trigonometry/ @@ -130,7 +130,7 @@ https://openfreebooks.org/math/percentage-applications/ - 2026-05-21T17:43:39.595Z + 2026-05-21T17:54:08.000Z https://openfreebooks.org/math/permutations-combinations-probability/ @@ -154,11 +154,11 @@ https://openfreebooks.org/math/rational-number-operations/ - 2026-05-21T17:43:39.472Z + 2026-05-21T17:54:08.000Z https://openfreebooks.org/math/ratios-and-rates-foundation/ - 2026-05-21T17:43:39.210Z + 2026-05-21T17:54:08.000Z https://openfreebooks.org/math/sequences-series/ diff --git a/themes/openfreebooks/sass/_components.scss b/themes/openfreebooks/sass/_components.scss index 50348e0..75241be 100644 --- a/themes/openfreebooks/sass/_components.scss +++ b/themes/openfreebooks/sass/_components.scss @@ -345,6 +345,28 @@ h3 { margin-bottom: 12px; } +.sponsors-intro { + max-width: 680px; + margin: -10px 0 20px; + color: var(--soft-ink); +} + +.sponsors-cta { + margin: 12px 0 0; + color: var(--soft-ink); + font-size: 0.95rem; +} + +.sponsors-cta a { + color: var(--accent); + text-decoration: none; +} + +.sponsors-cta a:hover, +.sponsors-cta a:focus-visible { + text-decoration: underline; +} + .tree-cta { min-height: clamp(680px, 92vh, 980px); display: flex; diff --git a/themes/openfreebooks/sass/_contributing.scss b/themes/openfreebooks/sass/_contributing.scss index 8e136bc..4bb9c20 100644 --- a/themes/openfreebooks/sass/_contributing.scss +++ b/themes/openfreebooks/sass/_contributing.scss @@ -7,6 +7,30 @@ margin-bottom: 16px; } +.contributing-page__thanks { + max-width: var(--prose-width); + margin: 0 0 24px; + color: var(--soft-ink); + font-size: 1rem; + line-height: 1.7; + + p { + margin: 0; + } + + a { + color: var(--accent); + font-weight: 600; + text-decoration: none; + } + + a:hover, + a:focus-visible { + text-decoration: underline; + text-underline-offset: 3px; + } +} + .contributing-page__lead { margin: 0 0 36px; max-width: var(--prose-width); diff --git a/themes/openfreebooks/templates/contributing.html b/themes/openfreebooks/templates/contributing.html index c4cffe7..32f98c1 100644 --- a/themes/openfreebooks/templates/contributing.html +++ b/themes/openfreebooks/templates/contributing.html @@ -8,7 +8,11 @@ {% block content %}
+ {% if page.content %} +
{{ page.content | safe }}
+ {% endif %} {% include "partials/contributing-content.html" %}
+{% include "partials/sponsors-section.html" %} {% endblock content %} diff --git a/themes/openfreebooks/templates/credits.html b/themes/openfreebooks/templates/credits.html index e9057d6..c0d8100 100644 --- a/themes/openfreebooks/templates/credits.html +++ b/themes/openfreebooks/templates/credits.html @@ -13,4 +13,5 @@ {% include "partials/credits-content.html" %} +{% include "partials/sponsors-section.html" %} {% endblock content %} diff --git a/themes/openfreebooks/templates/index.html b/themes/openfreebooks/templates/index.html index ac15041..5997f81 100644 --- a/themes/openfreebooks/templates/index.html +++ b/themes/openfreebooks/templates/index.html @@ -43,6 +43,8 @@ {% include "partials/community-section.html" %} +{% include "partials/sponsors-section.html" %} +

Catalog

diff --git a/themes/openfreebooks/templates/partials/credits-content.html b/themes/openfreebooks/templates/partials/credits-content.html index f1dac1c..03f64b3 100644 --- a/themes/openfreebooks/templates/partials/credits-content.html +++ b/themes/openfreebooks/templates/partials/credits-content.html @@ -1,4 +1,5 @@ {% set credits = load_data(path="data/credits.json", format="json") %} +{% set sponsors_data = load_data(path="data/github-sponsors.json", format="json") %} {% set libraries = credits.libraries | sort(attribute="name") %}
@@ -7,6 +8,10 @@ {{ credits.contributors_note }} View contributors.

+

+ Special thanks to our GitHub Sponsors for helping keep this project free and open. + Sponsor Open Free Books. +

This project

diff --git a/themes/openfreebooks/templates/partials/sponsors-section.html b/themes/openfreebooks/templates/partials/sponsors-section.html new file mode 100644 index 0000000..fa00e4f --- /dev/null +++ b/themes/openfreebooks/templates/partials/sponsors-section.html @@ -0,0 +1,42 @@ +{% set sponsors_data = load_data(path="data/github-sponsors.json", format="json") %} + +
+
+

Support

+

{{ sponsors_data.title }}

+

{{ sponsors_data.intro }}

+ + {% if sponsors_data.sponsors and sponsors_data.sponsors | length > 0 %} +
    + {% for sponsor in sponsors_data.sponsors %} +
  • + + + +
  • + {% endfor %} +
+

+ Thank you to all sponsors +

+ {% else %} +

+ No public sponsors listed yet. Sponsor on GitHub. +

+ {% endif %} +
+