Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,19 @@
"mcp__claude_ai_Postiz__integrationList",
"mcp__Mzizi__mzizi_get_tokens"
]
},
"hooks": {
"PostToolUse": [
{
"matcher": "mcp__github__merge_pull_request",
"hooks": [
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/scripts/clean.mjs\" --hook",
"timeout": 120
}
]
}
]
}
}
5 changes: 5 additions & 0 deletions .githooks/post-merge
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
# After a `git pull` that merges: keep the clone light (scripts/clean.mjs).
# Turned on by `npm ci` (scripts/install-hooks.mjs). It never fails a pull.
command -v node >/dev/null 2>&1 || exit 0
node scripts/clean.mjs --quiet || true
20 changes: 20 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Keeps the actions pinned in .github/workflows current. They are pinned to
# full commit SHAs, with the version in a comment, so a moved tag cannot
# change what runs; Dependabot updates the SHA and the comment together.
#
# npm dependencies are not listed here: no entry existed before, and adding
# one is a separate decision about how many pull requests this repo wants.
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
# A release has a week to be pulled or found bad before a pin moves to it.
cooldown:
default-days: 7
groups:
actions:
patterns: ["*"]
commit-message:
prefix: ci
48 changes: 28 additions & 20 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ on:
push:
branches: [main, staging]

permissions:
contents: read
# Nothing at the workflow level: each job asks for what it uses, and no job
# here pushes, tags or comments, so each reads the repository and no more.
permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand All @@ -15,11 +16,17 @@ concurrency:
jobs:
check:
name: astro check + tests + build
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
# Nothing here pushes, so the token is not left in .git/config for
# every later step (and every npm install script) to read.
persist-credentials: false

- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "22"
cache: "npm"
Expand All @@ -30,20 +37,26 @@ jobs:
- name: Type-check
run: npm run check

# `npm test` runs vitest, builds the site, and then verifies that every
# inline script in the build is covered by the CSP hash allowlist in
# vercel.json — so an edit to the theme bootstrap cannot ship a page the
# browser will refuse to run.
# `npm test` builds the site, runs the tests (vp test), and then verifies
# that every inline script in the build is covered by the CSP hash
# allowlist in vercel.json — so an edit to an inline script cannot ship
# a page the browser will refuse to run.
- name: Tests, build, and CSP check
run: npm test

audit:
name: npm audit
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
# Nothing here pushes, so the token is not left in .git/config for
# every later step (and every npm install script) to read.
persist-credentials: false

- uses: actions/setup-node@v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "22"
cache: "npm"
Expand All @@ -62,16 +75,11 @@ jobs:
- name: Audit what ships
run: node scripts/audit.mjs

# Build and test tooling, reported but not blocking.
#
# vitest currently carries an advisory with no installable fix: every
# published version up to 4.1.10 is affected, and 4.1.11 — the fix —
# cannot be installed, because it trips a resolver bug in npm 10.9
# ("Cannot read properties of null (reading 'edgesOut')") that aborts
# `npm install` outright. Blocking on it would pin this job red until
# npm ships a fix, which trains everyone to ignore it. So it is surfaced
# in the log instead, and this step goes back to blocking the moment
# vitest 4.1.11 installs cleanly.
# Build and test tooling, reported but not blocking. None of it ships:
# the site is static HTML. Blocking on an advisory in a build tool with
# no non-breaking fix would pin this job red and train everyone to
# ignore it, so it is surfaced in the log instead. Today that is
# postcss-selector-parser, through @tailwindcss/typography.
- name: Report advisories in build and test tooling
continue-on-error: true
run: npm audit --audit-level=moderate
5 changes: 3 additions & 2 deletions .github/workflows/main-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ on:
options: [minor, major]
default: minor

permissions:
contents: read
# Nothing at the workflow level: the one job below asks for the write access
# that tagging needs, and only it.
permissions: {}

jobs:
version:
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/staging-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ on:
options: [patch, minor, major]
default: patch

permissions:
contents: read
# Nothing at the workflow level: the one job below asks for the write access
# that tagging needs, and only it.
permissions: {}

jobs:
version:
Expand Down
106 changes: 106 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,66 @@ numbers below were the site's earlier, hand-kept numbering and are retired; the

## Unreleased

### Added

- **Mukoko Lingo has its own page, `/mukoko-lingo`.** Mukoko Lingo is a
partner app developed by Nyuchi Learning, part of Mukoko, Africa's super
app. The page says what it does today, who it is for, what schools get
and how to start, in the extension page's style, with the app's own icon.
Every claim is from Lingo's public repository (mukoko-dev/mukoko-lingo:
README.md, app.json, BRANDING.md and the app's own copy). It sits beside
the Toddle Enhancement Extension wherever the extension appears: a second
product card on the home page, "Mukoko Lingo" in the header (where
"Tools" is now "Toddle extension"), and the footer's "Tools" column, now
"Products". It has a SoftwareApplication node in its JSON-LD, Open Graph
tags, a sitemap entry and an entry under Products in llms.txt.
`mukokoLingo` in `src/data/site.ts` holds its links.
`tests/seo.test.ts` checks all of that.
- **`npm run clean`, and after every merge.** `scripts/clean.mjs` removes
the build output (`dist/`, `.astro/`, `.vercel/output/`), the QA report,
the tools' caches, finished Claude Code worktrees and local branches
already in `main` or `staging`, touching only what Git ignores. It runs
after a `git pull` that merges (`.githooks/post-merge`, turned on by
`npm ci` through the new `prepare` script) and after Claude Code merges a
pull request (`.claude/settings.json`). See CONTRIBUTING.md.

### Changed

- **Mukoko Lingo is written for organisations first.** The page, the
homepage card and `llms.txt` now lead with schools, businesses and
government, then single learners. The "For schools" section is "For
schools, businesses and government" (anchor `#organisations`), and the
enquiry button reads "Talk to us about your organisation". Lingo is
described as "Part of Mukoko by Nyuchi", and Swahili is shown as beta
(owner decisions, 2026-10-15).

- **Mukoko Lingo, described as it is.** The home page card and llms.txt
said Lingo was "free for personal use", "Shona and Ndebele primary" and
taught French and Portuguese. Its repository says it teaches Shona,
Ndebele, English and Chinese, and that starting is free; the copy now
says that, and links to the live app at lingo.mukoko.com rather than
mukoko.com/lingo. The home page's "three surfaces" are now the two
products and two destinations.
- **0.11.0 is on the Chrome Web Store, and is the current version (#85).**
It is 0.10.0 with a security fix to how licence keys are checked. The
Releases page lists it first, marked on the store, from the extension's
CHANGELOG; the test builds tagged since 0.10.0 (0.10.1 to 0.10.5, 0.11.1
and 0.11.2) open its notes, and 0.12.0, not released, is not listed.
`extensionVersions.current` is 0.11.0; the new `changedIn` (0.9.0) keeps
"versions before 0.9.0" true where the old licence host and flags key are
named. The product page, the Security page (which now describes the
0.11.0 fix), the Releases description and llms.txt name 0.11.0. The
anchors for 0.9.6 and 0.9.7 the issue asked for were already there; the
test that every tagged test build has one now covers the 0.10.x and
0.11.x builds too.
- **`webStore`'s type is a named `Url`**, so Prettier 3.3 and 3.9 format
`src/data/site.ts` the same way; the org lint gate (3.9.4) had flagged it.
- **After paying: the checkout note describes today's email, a licence
key.** The claim-link wording (#89) is held back until the licence server
sends claim links, which ships with extension 0.12.0. Until then buyers
still receive the key by email, so the note says that. Restore #89 when
0.12.0 and the server's claim links go live.
- **0.10.0 is on the Chrome Web Store.** The Releases page shows it as on the store since 13 October 2026, and the test builds 0.9.9 and 0.9.10 open its notes.
- **The Toddle Enhancement Extension 0.9.0 is on the Chrome Web Store**,
published on 7 October 2026. The Releases page marks it "On the Chrome Web
Store", dated 7 October 2026. `extensionVersions.current` is 0.9.0 and
Expand All @@ -22,6 +80,54 @@ numbers below were the site's earlier, hand-kept numbering and are retired; the
from 0.8.2 stays, as history: flags it hid stay concealed, and versions
before 0.9.0 used the storage key `gbx-hide-flags` and asked the licence
host `licences.nyuchi.dev`.
- **Google Analytics loads only after consent (#83).** Google's tag is no
longer on the page behind Consent Mode set to denied, which still sent
Google a cookieless ping; it is not requested at all until the visitor
agrees. `Analytics.astro` queues the denied defaults and holds the only
loader, which runs on a stored yes from the current banner version or when
`CookieBanner.astro` records one. A record from an older version, the first
banner's `analytics-consent` key, or anything malformed counts as no answer,
and the banner asks again; `SupportWidget.astro` applies the same check
through `parseConsent` in `src/data/consent.ts`. The privacy and cookie
policies now say that Google Analytics contacts Google only after you agree
(Google Fonts is still named separately), dated 13 October 2026, as is
llms.txt. The CSP has one inline-script hash fewer. `tests/consent.test.ts`
runs the built script against current, old, legacy and malformed records,
and `npm run consent:probe` now fails on any Google Analytics request
before consent.
- **CI asks for less (#84).** Every workflow grants nothing at the top level;
each job asks for its own permissions (`contents: read` for the build and
audit jobs, `contents: write` only for the two tagging jobs). The build
workflow's checkouts set `persist-credentials: false`, since nothing there
pushes. `actions/checkout` and `actions/setup-node` are pinned to full
commit SHAs (v4.4.0 each), with the version in a comment, and the new
`.github/dependabot.yml` keeps those pins current, a week after each
release (`cooldown`). The reusable workflows
from `nyuchi/.github` were already pinned to a commit.
- **Docs match the site as it is (#86).** README, CONTRIBUTING and
`docs/toolchain.md` describe the Astro site's pages, the staging → main
release flow, Vite+ as adopted (no longer "cannot be adopted"), the inline
scripts the CSP hashes, and the current audit; the dead Lint badge, the
vitest pin, `master` and "two pages" are gone. `react` and `react-dom` are
no longer dev dependencies: nothing imports them, and `@bundu/ui` declares
no peer dependencies. Prettier and `vp fmt` both stay, because each is run
by a required org gate; `tests/toolchain.test.ts` fails if their settings
differ.

### Fixed

- **Dependency advisories.** `http-cache-semantics` takes its patched
release (GHSA-ch52-4w7c-c8xp), so its dated exception in
`scripts/audit.mjs` is gone. `postcss-selector-parser` is held at 7.1.6 or
later through an npm `overrides` entry (GHSA-rj75-hqrm-r3gf): the
typography plugin pins 6.0.10, and the built CSS is byte-for-byte the same
with the newer version.
- **Anchors for the extension's test builds 0.9.6, 0.9.7 and 0.9.8 (#85)**
on the Releases page, under 0.10.0 beside 0.9.1 to 0.9.5, so the links from
those builds' menus land on the release they lead to. 0.10.0 is still
"Coming soon to the Chrome Web Store". `taggedTestBuilds` in
`src/data/releases.ts` lists every tagged test build, and a test fails the
build if one has no anchor, or more than one.

## 0.1.0 — 2026-10-07

Expand Down
73 changes: 54 additions & 19 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ three predictable places.

## What gets changed where

| Change | File |
| --------------------------------------------------------- | ----------------------------------------------------- |
| Nav links, footer columns, legal links, the wordmark | `src/data/site.ts` |
| Links to the extension (repo, releases, deployment guide) | `src/data/site.ts` (`toddleExtension`) |
| The three destination cards | the `surfaces` array in `src/pages/index.astro` |
| Home page copy | `src/pages/index.astro` |
| Extension page copy | `src/pages/toddle-enhancement-extension.astro` |
| Header / footer structure | `src/components/SiteHeader.astro`, `SiteFooter.astro` |
| `<head>`, metadata, theme bootstrap | `src/layouts/BaseLayout.astro` |
| Change | File |
| ------------------------------------------------------- | ------------------------------------------------------------------- |
| Nav links, footer columns, legal links, the wordmark | `src/data/site.ts` |
| Links to the extension (store listing, releases, guide) | `src/data/site.ts` (`toddleExtension`) |
| The extension's release notes | `src/data/releases.ts` |
| Facts on the legal pages (versions, prices, contacts) | `src/data/legal.ts` |
| Consent categories and the banner's version | `src/data/consent.ts` |
| The destination cards | the `surfaces` array in `src/pages/index.astro` |
| Home page copy | `src/pages/index.astro` |
| Extension page copy | `src/pages/toddle-enhancement-extension.astro` |
| Mukoko Lingo's links / page copy | `src/data/site.ts` (`mukokoLingo`) / `src/pages/mukoko-lingo.astro` |
| Header / footer structure | `src/components/SiteHeader.astro`, `SiteFooter.astro` |
| `<head>`, metadata, theme bootstrap | `src/layouts/BaseLayout.astro` |

## What must not be changed here

Expand All @@ -38,16 +42,38 @@ lowercase spelling anywhere in `src/` fails that test too.
npm install
npm run dev # http://localhost:4321 — hot reload
npm run check # astro check
npm test # vitest, then build, then the CSP hash check
npm test # build, then the tests (vp test), then the CSP hash check
npm run lint # vp lint
npm run format # prettier --write
npm run format:check # the org lint gate runs this
npm run fmt # vp fmt; the org vite-plus gate runs `vp fmt --check`
```

## If you touch the theme bootstrap
## Keeping the clone light

The one inline script in `BaseLayout.astro` is allowed by a hash in the
Content-Security-Policy in `vercel.json`. Change the script and the hash no
longer matches, so:
`npm run clean` (`scripts/clean.mjs`) removes what the build and the tools
generate (`dist/`, `.astro/`, `.vercel/output/`, `qa-report.json`, the tools' caches), Claude Code worktrees with nothing uncommitted, and local
branches that are done: everything on them is already in `origin/main` or
`origin/staging` (a squash merge included), or their remote branch was
deleted after the pull request merged while they held nothing unpushed. It
touches only what Git ignores, never `main`, `staging`, the branch checked
out or one in a worktree, and names anything it leaves. `--dry-run` lists,
`--deep` also removes `node_modules` and npm's cache, `--no-fetch` works
offline.

It runs on its own after a `git pull` that merges (`.githooks/post-merge`,
turned on by `npm ci`), and after Claude Code merges a pull request
(`.claude/settings.json`, a `PostToolUse` hook) in whichever of the three
clones (`toddle-enhancement-extension`, `learning`, `nyuchi-licence-server`)
the pull request belongs to. The same script is in all three; only its list
of generated paths differs.

## If you touch an inline script

The inline scripts (the theme bootstrap in `BaseLayout.astro`, the consent
check in `Analytics.astro`, and the small modules Astro inlines) are allowed by
hashes in the Content-Security-Policy in `vercel.json`. Change one and its hash
no longer matches, so:

```sh
npm run build
Expand All @@ -56,8 +82,12 @@ npm run csp:fix # rewrite vercel.json with the new hashes

Then review the diff — a changed hash should correspond to a change you meant
to make. `npm test` fails if you skip this, which is the point: a stale hash
would mean the browser refuses to run the script and the theme silently breaks
in production.
would mean the browser refuses to run the script and it silently breaks in
production.

If you touch the consent banner or analytics, run `npm run consent:probe` too:
Google's tag must not be requested before the visitor agrees, and the privacy
policy says so.

## Adding a page

Expand All @@ -68,9 +98,14 @@ in production.

## Pull requests

CI runs `astro check`, the tests, the build and the CSP check, plus the org
lint gate (actionlint, JSON validity, Prettier, markdownlint, yamllint). All of
it has to be green. Merging to `master` deploys to Vercel.
Branch from `staging` and open the pull request against `staging`. CI runs
`astro check`, the build, the tests and the CSP check, plus the org gates: the
lint gate (actionlint, JSON validity, Prettier, markdownlint, yamllint) and
`vite-plus / check` (`astro check`, `vp fmt --check`). All of it has to be
green. Each merge into `staging` is tagged as the next patch. A release is a
pull request from `staging` to `main`: production deploys from `main`, which
is tagged as the next minor. Add a line under "Unreleased" in CHANGELOG.md
for anything a reader of the site, or of this repo, would notice.

Do not add a link to the extension's source repository — it is private, and a
test fails the build on any `github.com` link for that reason.
Expand Down
Loading
Loading