diff --git a/.agents/skills/ofb/SKILL.md b/.agents/skills/ofb/SKILL.md index b8ad8af..939edd6 100644 --- a/.agents/skills/ofb/SKILL.md +++ b/.agents/skills/ofb/SKILL.md @@ -33,8 +33,9 @@ frontend/ → catalog-app, canvas viewer, site chrome ```bash bun install bun run build:js # after frontend/ changes +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 → public/ +bun run build # production: JS + Zola + pagefind → public/ ``` ## Non-negotiables @@ -59,6 +60,21 @@ bun run build # production: JS + Zola → public/ | New chapter (live) | Above + `content/{subject}/{slug}/_index.md` + HTML partial + `chapter.html` wiring + `main.tsx` widget mount if interactive | | 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 | + +## Search (Pagefind) + +| Piece | Path | +|-------|------| +| Global modal + assets | `themes/.../templates/base.html` | +| Dedicated page | `/search/` — `content/search.md`, `templates/search.html` | +| Header trigger | `frontend/src/components/site-header.tsx` (`pagefind-modal-trigger`) | +| Nav link | `main.tsx` + `zola.toml` `search_url` | +| Theming | `themes/.../sass/_search.scss` (`--pf-*` → site tokens) | + +- **Component UI** only (`pagefind-component-ui.js`), not legacy `pagefind-ui.js`. +- Index runs **after** `zola build`. Production: `bun run build`. Dev: `bun run index:search` copies bundle to `static/pagefind/` (gitignored) for `zola serve`. +- v1 indexes **live chapter pages only** (`data-pagefind-body` on `templates/math/chapter.html`); home, about, catalog, credits, and other site pages are excluded. Detailed checklists: [curriculum-data.md](curriculum-data.md). Human-facing guide: [CONTRIBUTING.md](../../../CONTRIBUTING.md). @@ -114,7 +130,6 @@ bun run build ## Planned / not implemented (do not assume) -- Pagefind search (`build_search_index = false`) - Defuddle “copy as Markdown” on chapters - Markdown-based chapters (spec says HTML; README may be outdated) - Data-driven homepage cards diff --git a/.agents/skills/zola/SKILL.md b/.agents/skills/zola/SKILL.md index 4a798d4..6e408dc 100644 --- a/.agents/skills/zola/SKILL.md +++ b/.agents/skills/zola/SKILL.md @@ -29,12 +29,16 @@ See [reference.md](reference.md) for Tera pitfalls and build checks. ```bash bun run build:js # Vite → themes/openfreebooks/static/js/ (bundle.js + lazy chunks) +zola build # required before indexing +bun run index:search # pagefind on public/ → copies to static/pagefind/ for zola serve zola serve # dev at http://127.0.0.1:1111 -bun run build # build:js + zola build → public/ +bun run build # build:js + zola build + index:search ``` After Sass or template changes, run `zola build` (or `zola serve`). Do **not** copy `public/` into `themes/openfreebooks/static/`. +**Search (Pagefind):** read [zola-pagefind.md](zola-pagefind.md) before changing search UI, indexing, or `data-pagefind-*` attributes. + ## Templates (Tera) - Extend `base.html`; use `{% block content %}` for page bodies. @@ -75,6 +79,11 @@ Client behavior lives in `frontend/` and mounts from `base.html`: **When changing `frontend/` or adding interactivity**, read [zola-solid.md](zola-solid.md) before editing JS. +### Search (Pagefind) + +- Runs after `zola build`; indexes **live chapter pages** only (`data-pagefind-body` on chapter template). +- UI: global modal (⌘K) + `/search/` page. See [zola-pagefind.md](zola-pagefind.md). + ### Catalog page - `content/catalog.md` → `template = "catalog.html"` diff --git a/.agents/skills/zola/zola-pagefind.md b/.agents/skills/zola/zola-pagefind.md new file mode 100644 index 0000000..d76ad3e --- /dev/null +++ b/.agents/skills/zola/zola-pagefind.md @@ -0,0 +1,170 @@ +# Pagefind + Zola (Open Free Books) + +Reference for static search in this repo. Pagefind runs **after** Zola builds HTML to `public/` — it does not use Zola’s `build_search_index`. + +Read [Pagefind Component UI](https://pagefind.app/llms-component-ui.txt) for upstream API details. Use **Component UI** (`pagefind-component-ui.js`) only — not legacy `pagefind-ui.js` / `PagefindUI`. + +## How it fits together + +```text +bun run build:js → themes/openfreebooks/static/js/bundle.js +zola build → public/**/*.html +pagefind --site public → public/pagefind/ (index + WASM + UI assets) +``` + +| Environment | Index output | Served from | +|-------------|--------------|-------------| +| Production (`bun run build`) | `public/pagefind/` | Deployed with Wrangler | +| Dev (`zola serve`) | `static/pagefind/` (copy) | Zola copies `static/` to site root | + +`static/pagefind/` is **gitignored**. Run `bun run index:search` after `zola build` so `zola serve` can load `/pagefind/*`. + +## Commands + +```bash +bun run build:js # if frontend changed +zola build # required before indexing +bun run index:search # pagefind on public/ → copy to static/pagefind/ +zola serve # http://127.0.0.1:1111 +``` + +Production (includes index + copy): + +```bash +bun run build # build:js + zola build + index:search +``` + +`package.json` devDependency: `pagefind` (^1.5). CLI: `pagefind --site public`. + +## What gets indexed + +**Live chapter pages only.** Once any page uses `data-pagefind-body`, Pagefind **skips** all pages without it. + +| Source | Indexed? | +|--------|----------| +| `templates/math/chapter.html` → `
` | Yes | +| Home, about, catalog, credits, contributing, `/search/` | No | +| Catalog chapter cards (Solid + JSON) | No | +| Planned chapters (no `content/` page) | No | + +New subject chapter templates must include `data-pagefind-body` on the main lesson container. + +### Per-chapter markup + +[`themes/openfreebooks/templates/math/chapter.html`](../../../themes/openfreebooks/templates/math/chapter.html): + +```html +
+ … +
+
+
+``` + +- **`data-pagefind-body`** — title, lead, and HTML partial prose. +- **`data-pagefind-ignore`** — chrome and interactive widgets (not lesson text). + +Breadcrumb nav stays **outside** the article so it is not indexed. + +## UI integration + +### Global (every page) + +[`themes/openfreebooks/templates/base.html`](../../../themes/openfreebooks/templates/base.html): + +- ``: `pagefind/pagefind-component-ui.css` + `.js` via `get_url()` +- Before ``: `` +- `#site-config` includes `searchUrl` from `zola.toml` `[extra] search_url` + +### Header (Solid) + +[`frontend/src/components/site-header.tsx`](../../../frontend/src/components/site-header.tsx): + +```tsx + +``` + +[`frontend/src/main.tsx`](../../../frontend/src/main.tsx): nav link **Search** → `config.searchUrl`. + +Types: [`frontend/src/types/pagefind-components.d.ts`](../../../frontend/src/types/pagefind-components.d.ts). + +### Dedicated page + +| File | Role | +|------|------| +| `content/search.md` | `template = "search.html"` | +| `templates/search.html` | `pagefind-input`, `pagefind-summary`, `pagefind-results`, `pagefind-keyboard-hints` | +| `sass/_search.scss` | Layout + map `--pf-*` to site tokens (`:root`, `[data-theme="dark"]`) | + +URL: `/search/`. Modal and page share the default Pagefind instance (⌘K / Ctrl+K). + +## Theming + +Pagefind hosts use `all: initial` — site CSS does not cascade in. Theme via **`--pf-*`** variables in `_search.scss` (see [Pagefind CSS variables](https://pagefind.app/docs/css-variables/)). + +**Load order in `base.html`:** `pagefind-component-ui.css` first, then `main.css` so site `--pf-*` overrides Pagefind defaults (white button, blue focus ring). + +Site theme uses `data-theme` and `data-pf-theme` on `` (`theme.ts`); tokens map under `[data-theme="dark"]` and `prefers-color-scheme`. Header trigger scoped in `.site-header__search` to match `.theme-toggle`. + +## Zola config + +[`zola.toml`](../../../zola.toml): + +```toml +build_search_index = false # Zola search unused; Pagefind handles search +search_url = "/search/" +``` + +`` in `base.html` — required for Pagefind language detection. + +## Stale static HTML (common bug) + +**Never** copy `public/` into `themes/openfreebooks/static/`. Committed `themes/openfreebooks/static/**/*.html` **overrides** Zola output and breaks: + +- `data-pagefind-body` on chapters +- Pagefind assets in `base.html` +- Any template change + +If search indexes 8 pages instead of 1, delete stale `themes/openfreebooks/static/**/*.html`, then `zola build` and `bun run index:search`. + +Allowed under theme `static/`: built JS (`js/bundle.js`, hashed chunks), fonts, etc. — not generated HTML. + +## Verify + +```bash +bun run build:js && zola build && bun run index:search +``` + +Expect Pagefind output similar to: + +```text +Indexed 1 page +Indexed … words +``` + +Check: + +- `public/math//index.html` contains `data-pagefind-body` +- `public/pagefind/pagefind-component-ui.js` exists +- `static/pagefind/` exists after `index:search` (for dev) +- `/search/` and header ⌘K return chapter hits only + +## Adding a new live chapter + +1. Publish chapter per [CONTRIBUTING.md](../../../CONTRIBUTING.md) (`content/`, partial, `chapter.html` branch). +2. Ensure content lives inside `
`. +3. `zola build && bun run index:search` (or full `bun run build`). +4. Confirm indexed page count increased. + +## Do not + +- Commit `public/pagefind/` or `static/pagefind/`. +- Use `pagefind-ui.js` / `new PagefindUI()`. +- Add `data-pagefind-body` to `base.html` or marketing pages unless you intend to index them. +- Expect catalog JSON titles in search without a static HTML index block (future enhancement). + +## Related + +- [SKILL.md](SKILL.md) — Zola layout and commands +- [zola-solid.md](zola-solid.md) — header mount, `searchUrl` in site config +- [ofb/SKILL.md](../ofb/SKILL.md) — product rules, chapter workflow diff --git a/.gitignore b/.gitignore index 5a09e38..9d8fcfe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Zola build output (Wrangler deploy source) public/ +# Pagefind index copied for zola serve (run bun run index:search) +static/pagefind/ + # Dependencies node_modules/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 132cb13..f3aa55a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,6 +20,8 @@ Thank you for helping grow free, open textbooks. **Browse the catalog locally:** `bun run build:js && zola serve` → [http://127.0.0.1:1111/catalog/](http://127.0.0.1:1111/catalog/) +**Search locally:** After `bun run build:js`, run `bun run index:search` once (or again after changing chapter HTML), then `zola serve`. Try [http://127.0.0.1:1111/search/](http://127.0.0.1:1111/search/) or press ⌘K / Ctrl+K. Pagefind indexes **live chapter pages only** (not home, about, catalog, or credits). Without `index:search`, the search UI loads but returns no results. + --- ## How the catalog fits together @@ -110,7 +112,7 @@ chapter_id = "your-chapter-slug" Today, chapter bodies live as HTML partials under the theme, not Markdown: 1. Add `themes/openfreebooks/templates/partials/math/your-chapter-slug-content.html` with the lesson HTML. -2. In `themes/openfreebooks/templates/math/chapter.html`, include it when `chapter_id` matches: +2. In `themes/openfreebooks/templates/math/chapter.html`, include it when `chapter_id` matches (chapter content is inside `
` for search indexing): ```html {% if section.extra.chapter_id == "your-chapter-slug" %} diff --git a/README.md b/README.md index df58426..823ef5a 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Free, open-source textbooks for every learner — from elementary school through | Theme | Custom theme in `themes/openfreebooks/` | | Interactivity | [Solid.js](https://www.solidjs.com/) (header, marquee, scroll reveal, theme toggle) | | JS build | [Bun](https://bun.sh/) + [Vite](https://vite.dev/) | +| Search | [Pagefind](https://pagefind.app/) (post-build index, Component UI) | | Hosting | Cloudflare Workers (static assets via Wrangler) | Chapter content is **HTML partials** in the theme today (not Markdown). Canvas maps use the [JSON Canvas](https://jsoncanvas.org/) standard (Obsidian-compatible) via [json-canvas-viewer](https://github.com/Hesprs/JSON-Canvas-Viewer). @@ -47,14 +48,17 @@ Install JS dependencies: bun install ``` -Build the Solid bundle and start Zola’s dev server: +Build the Solid bundle, index search (once per session or after content changes), then start Zola’s dev server: ```bash bun run build:js +bun run index:search zola serve ``` -Open [http://127.0.0.1:1111](http://127.0.0.1:1111). Re-run `bun run build:js` (or `bun run dev:js` in another terminal) when you change `frontend/`. +Open [http://127.0.0.1:1111](http://127.0.0.1:1111). Re-run `bun run build:js` (or `bun run dev:js` in another terminal) when you change `frontend/`. Re-run `bun run index:search` when you change chapter HTML or add pages so search stays up to date. + +Search UI is available immediately; results require the index step above (`static/pagefind/` is gitignored and copied from the build output). ## Production build @@ -62,7 +66,7 @@ Open [http://127.0.0.1:1111](http://127.0.0.1:1111). Re-run `bun run build:js` ( bun run build ``` -This runs `vite build` (outputs to `themes/openfreebooks/static/js/bundle.js`) and then `zola build` (outputs to `public/`). +This runs `vite build` (outputs to `themes/openfreebooks/static/js/bundle.js`), `zola build` (outputs to `public/`), then Pagefind (`public/pagefind/`). ## Deploy to Cloudflare @@ -78,7 +82,7 @@ The Worker name is `openfreebooks` (see `wrangler.jsonc`). Assets are served fro Site-wide settings live in `zola.toml`: - `base_url` — canonical URL for feeds and absolute links -- `[extra]` — `site_title`, `github_url`, `browse_url`, `about_url` (used in templates and the Solid config block) +- `[extra]` — `site_title`, `github_url`, `browse_url`, `search_url`, `about_url` (used in templates and the Solid config block) ## Theme notes @@ -108,7 +112,7 @@ These issues make contributions harder than they should be. PRs that fix any ite ### Site & discoverability - [ ] **Data-driven homepage cards** — “Browse by subject” / featured blocks duplicate manual edits in `index.html`. -- [ ] **Pagefind search** — spec mentions search; `build_search_index = false` and no index step in build. +- [x] **Pagefind search** — `/search/`, header modal (⌘K), `bun run index:search` for local dev. - [ ] **CONTRIBUTING wizard or checklist in CI** — fail PR if live chapter missing partial or catalog entry. ### Build & repo hygiene diff --git a/bun.lock b/bun.lock index ef37cca..2cf57c7 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "three": "^0.184.0", }, "devDependencies": { + "pagefind": "^1.3.0", "typescript": "^5.8.3", "vite": "^6.3.5", "vite-plugin-solid": "^2.11.6", @@ -132,6 +133,20 @@ "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], + "@pagefind/darwin-arm64": ["@pagefind/darwin-arm64@1.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ=="], + + "@pagefind/darwin-x64": ["@pagefind/darwin-x64@1.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw=="], + + "@pagefind/freebsd-x64": ["@pagefind/freebsd-x64@1.5.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA=="], + + "@pagefind/linux-arm64": ["@pagefind/linux-arm64@1.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw=="], + + "@pagefind/linux-x64": ["@pagefind/linux-x64@1.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA=="], + + "@pagefind/windows-arm64": ["@pagefind/windows-arm64@1.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g=="], + + "@pagefind/windows-x64": ["@pagefind/windows-x64@1.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.4", "", { "os": "android", "cpu": "arm" }, "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.4", "", { "os": "android", "cpu": "arm64" }, "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw=="], @@ -446,6 +461,8 @@ "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + "pagefind": ["pagefind@1.5.2", "", { "optionalDependencies": { "@pagefind/darwin-arm64": "1.5.2", "@pagefind/darwin-x64": "1.5.2", "@pagefind/freebsd-x64": "1.5.2", "@pagefind/linux-arm64": "1.5.2", "@pagefind/linux-x64": "1.5.2", "@pagefind/windows-arm64": "1.5.2", "@pagefind/windows-x64": "1.5.2" }, "bin": { "pagefind": "lib/runner/bin.cjs" } }, "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], diff --git a/content/search.md b/content/search.md new file mode 100644 index 0000000..d573f60 --- /dev/null +++ b/content/search.md @@ -0,0 +1,4 @@ ++++ +title = "Search" +template = "search.html" ++++ diff --git a/frontend/src/components/site-header.tsx b/frontend/src/components/site-header.tsx index 9cea670..64d20b9 100644 --- a/frontend/src/components/site-header.tsx +++ b/frontend/src/components/site-header.tsx @@ -22,7 +22,7 @@ export function SiteHeader(props: SiteHeaderProps) { window.addEventListener("scroll", onScroll, { passive: true }); const onResize = () => { - if (window.innerWidth > 467 && navOpen()) { + if (window.innerWidth > 768 && navOpen()) { setNavOpen(false); document.body.style.overflow = ""; } @@ -60,6 +60,9 @@ export function SiteHeader(props: SiteHeaderProps) { +