Integrate Pagefind for enhanced search functionality: update build process to include search indexing, add search page and UI components, and modify templates for search integration. Update styles for search modal and ensure proper indexing of chapter content. Enhance documentation to reflect new search capabilities.

This commit is contained in:
Hananoshika Yomaru 2026-05-19 12:29:49 -07:00
parent c66f8850a9
commit 227404f623
23 changed files with 553 additions and 19 deletions

View file

@ -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

View file

@ -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"`

View file

@ -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` → `<article data-pagefind-body>` | 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
<article class="book-chapter" data-pagefind-body>
…
<div id="copy-page-button" … data-pagefind-ignore></div>
<div id="quadratic-explorer" … data-pagefind-ignore></div>
</article>
```
- **`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):
- `<head>`: `pagefind/pagefind-component-ui.css` + `.js` via `get_url()`
- Before `</body>`: `<pagefind-modal reset-on-close>`
- `#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
<pagefind-modal-trigger compact placeholder="Search" />
```
[`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 `<html>` (`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/"
```
`<html lang="en">` 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/<slug>/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 `<article data-pagefind-body>`.
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

3
.gitignore vendored
View file

@ -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/

View file

@ -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 `<article data-pagefind-body>` for search indexing):
```html
{% if section.extra.chapter_id == "your-chapter-slug" %}

View file

@ -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

View file

@ -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=="],

4
content/search.md Normal file
View file

@ -0,0 +1,4 @@
+++
title = "Search"
template = "search.html"
+++

View file

@ -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) {
<nav class="main-nav" aria-label="Primary navigation">
<For each={props.links}>{(link) => <a href={link.href}>{link.label}</a>}</For>
</nav>
<div class="site-header__search">
<pagefind-modal-trigger compact placeholder="Search" />
</div>
<ThemeToggle />
<button
type="button"

View file

@ -13,6 +13,7 @@ function readConfig() {
homeUrl: "/",
browseUrl: "/catalog/",
aboutUrl: "/about/",
searchUrl: "/search/",
contributingUrl: "/contributing/",
githubUrl: "https://github.com/openfreebooks/openfreebooks",
};
@ -22,6 +23,7 @@ function readConfig() {
homeUrl: string;
browseUrl: string;
aboutUrl: string;
searchUrl: string;
contributingUrl: string;
githubUrl: string;
};
@ -39,6 +41,7 @@ initTheme();
const config = readConfig();
const navLinks = [
{ label: "Browse", href: config.browseUrl },
{ label: "Search", href: config.searchUrl },
{ label: "About", href: config.aboutUrl },
{ label: "GitHub", href: config.githubUrl },
];

View file

@ -20,6 +20,7 @@ export function resolveTheme(): ThemeChoice {
export function applyTheme(theme: ThemeChoice) {
document.documentElement.setAttribute("data-theme", theme);
document.documentElement.setAttribute("data-pf-theme", theme);
}
export function setTheme(theme: ThemeChoice) {

View file

@ -0,0 +1,15 @@
import "solid-js";
declare module "solid-js" {
namespace JSX {
interface IntrinsicElements {
"pagefind-modal-trigger": {
compact?: boolean;
placeholder?: string;
shortcut?: string;
"hide-shortcut"?: boolean;
instance?: string;
};
}
}
}

BIN
og.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

View file

@ -5,7 +5,8 @@
"type": "module",
"scripts": {
"build:js": "vite build",
"build": "rm -f themes/openfreebooks/static/main.css public/main.css && bun run build:js && zola build",
"build": "rm -f themes/openfreebooks/static/main.css public/main.css && bun run build:js && zola build && bun run index:search",
"index:search": "pagefind --site public && rm -rf static/pagefind && cp -R public/pagefind static/pagefind",
"dev:js": "vite build --watch",
"scan:size": "bun scripts/scan-output-size.ts",
"test": "bun test frontend/src/lib"
@ -19,6 +20,7 @@
"three": "^0.184.0"
},
"devDependencies": {
"pagefind": "^1.5.0",
"typescript": "^5.8.3",
"vite": "^6.3.5",
"vite-plugin-solid": "^2.11.6"

View file

@ -440,7 +440,7 @@ h3 {
}
}
@media (max-width: 467px) {
@media (max-width: 768px) {
.nav-burger {
display: inline-flex;
margin-left: auto;

View file

@ -12,7 +12,8 @@ body::before {
/* Top-layer modals must stay opaque; grain blend would otherwise show through. */
.contributor-dialog[open],
.contributor-dialog[open] .contributor-dialog-panel {
.contributor-dialog[open] .contributor-dialog-panel,
pagefind-modal dialog.pf-modal[open] {
mix-blend-mode: normal;
}

View file

@ -0,0 +1,250 @@
// Pagefind Component UI — override defaults from pagefind-component-ui.css (load main.css after Pagefind).
// Modal surface matches .contributor-dialog / .contributor-dialog-panel (_contributors.scss).
$dialog-surface-light: #f5f4f1;
$dialog-surface-dark: #1a1917;
$dialog-backdrop-light: rgba(26, 26, 26, 0.45);
$dialog-backdrop-dark: rgba(0, 0, 0, 0.62);
@mixin pagefind-tokens {
--pf-text: var(--ink);
--pf-text-secondary: var(--soft-ink);
--pf-text-muted: var(--muted);
--pf-background: #{$dialog-surface-light};
--pf-border: var(--line);
--pf-border-focus: color-mix(in srgb, var(--accent) 45%, var(--line));
--pf-skeleton: color-mix(in srgb, var(--line) 50%, #{$dialog-surface-light});
--pf-skeleton-shine: color-mix(in srgb, var(--line) 25%, #{$dialog-surface-light});
--pf-hover: color-mix(in srgb, var(--accent) 10%, #{$dialog-surface-light});
--pf-mark: var(--accent);
--pf-scroll-shadow: var(--shadow-soft);
--pf-shadow-sm: 0 2px 8px var(--shadow-soft);
--pf-shadow-md: 0 4px 16px var(--shadow-soft);
--pf-shadow-lg: 0 18px 48px var(--shadow-soft);
--pf-error-bg: color-mix(in srgb, #dc2626 12%, var(--paper));
--pf-error-border: color-mix(in srgb, #dc2626 35%, var(--line));
--pf-error-text: #b91c1c;
--pf-error-text-secondary: #991b1b;
--pf-outline-focus: var(--accent);
--pf-outline-width: 2px;
--pf-outline-offset: 2px;
--pf-font: var(--font-body);
--pf-input-height: 40px;
--pf-input-font-size: 16px;
--pf-summary-font-size: 0.75rem;
--pf-result-title-font-size: 0.95rem;
--pf-result-excerpt-font-size: 0.85rem;
--pf-border-radius: 8px;
--pf-modal-backdrop: #{$dialog-backdrop-light};
--pf-modal-max-width: min(92vw, 440px);
--pf-modal-max-height: min(80dvh, 800px);
--pf-modal-top: 10dvh;
}
@mixin pagefind-dialog-dark-tokens {
--pf-background: #{$dialog-surface-dark};
--pf-skeleton: color-mix(in srgb, var(--line) 50%, #{$dialog-surface-dark});
--pf-skeleton-shine: color-mix(in srgb, var(--line) 25%, #{$dialog-surface-dark});
--pf-hover: color-mix(in srgb, var(--accent) 12%, #{$dialog-surface-dark});
--pf-modal-backdrop: #{$dialog-backdrop-dark};
}
:root {
@include pagefind-tokens;
}
[data-theme="dark"],
html[data-pf-theme="dark"] {
@include pagefind-tokens;
@include pagefind-dialog-dark-tokens;
--pf-error-bg: color-mix(in srgb, #dc2626 18%, var(--paper));
--pf-error-text: #fca5a5;
--pf-error-text-secondary: #fecaca;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
@include pagefind-tokens;
@include pagefind-dialog-dark-tokens;
--pf-error-bg: color-mix(in srgb, #dc2626 18%, var(--paper));
--pf-error-text: #fca5a5;
--pf-error-text-secondary: #fecaca;
}
}
// Match Pagefind's specificity so modal styles beat component defaults.
@mixin pf-host {
:is(*, #\#):is(*, #\#):is(*, #\#) {
@content;
}
}
pagefind-modal {
--pf-background: #{$dialog-surface-light};
--pf-modal-backdrop: #{$dialog-backdrop-light};
}
[data-theme="dark"] pagefind-modal,
html[data-pf-theme="dark"] pagefind-modal {
--pf-background: #{$dialog-surface-dark};
--pf-modal-backdrop: #{$dialog-backdrop-dark};
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) pagefind-modal {
--pf-background: #{$dialog-surface-dark};
--pf-modal-backdrop: #{$dialog-backdrop-dark};
}
}
@include pf-host {
dialog.pf-modal {
background-color: #{$dialog-surface-light};
color: var(--ink);
border: 1px solid var(--card-border);
border-radius: 0;
box-shadow: 0 18px 48px var(--shadow-soft);
isolation: isolate;
mix-blend-mode: normal;
}
dialog.pf-modal::backdrop {
background: #{$dialog-backdrop-light};
}
pagefind-modal-header,
.pf-modal-header,
pagefind-modal-body,
.pf-modal-body,
pagefind-modal-footer,
.pf-modal-footer {
background-color: #{$dialog-surface-light};
}
pagefind-modal-header .pf-input,
.pf-modal-header .pf-input {
background: transparent;
box-shadow: none;
}
.pf-modal-close {
width: 36px;
height: 36px;
border: none;
border-radius: 50%;
color: var(--muted);
font-size: 1.5rem;
line-height: 1;
}
.pf-modal-close:hover,
.pf-modal-close:focus-visible {
color: var(--ink);
background: var(--paper);
outline: none;
}
}
[data-theme="dark"] {
@include pf-host {
dialog.pf-modal {
background-color: #{$dialog-surface-dark};
}
dialog.pf-modal::backdrop {
background: #{$dialog-backdrop-dark};
}
pagefind-modal-header,
.pf-modal-header,
pagefind-modal-body,
.pf-modal-body,
pagefind-modal-footer,
.pf-modal-footer {
background-color: #{$dialog-surface-dark};
}
.pf-modal-close:hover,
.pf-modal-close:focus-visible {
background: color-mix(in srgb, var(--ink) 8%, #{$dialog-surface-dark});
}
}
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
@include pf-host {
dialog.pf-modal {
background-color: #{$dialog-surface-dark};
}
dialog.pf-modal::backdrop {
background: #{$dialog-backdrop-dark};
}
pagefind-modal-header,
.pf-modal-header,
pagefind-modal-body,
.pf-modal-body,
pagefind-modal-footer,
.pf-modal-footer {
background-color: #{$dialog-surface-dark};
}
}
}
}
// Header compact trigger — align with .theme-toggle
.site-header__search {
display: flex;
align-items: center;
--pf-background: transparent;
--pf-border: var(--line);
--pf-border-focus: color-mix(in srgb, var(--accent) 35%, var(--line));
--pf-hover: color-mix(in srgb, var(--accent) 10%, transparent);
--pf-text-muted: var(--soft-ink);
--pf-text-secondary: var(--soft-ink);
--pf-input-height: 36px;
--pf-border-radius: 8px;
--pf-font: var(--mono);
}
.search-page {
padding-block: clamp(2.5rem, 6vh, 4rem) clamp(3rem, 8vh, 5rem);
}
.search-page__layout {
max-width: 42rem;
}
.search-page__title {
margin-top: 0.35rem;
}
.search-page__lead {
margin: 0.75rem 0 2rem;
color: var(--soft-ink);
font-size: 1.05rem;
line-height: 1.55;
}
.search-page__ui {
display: flex;
flex-direction: column;
gap: 1rem;
--pf-background: #{$dialog-surface-light};
}
[data-theme="dark"] .search-page__ui {
--pf-background: #{$dialog-surface-dark};
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .search-page__ui {
--pf-background: #{$dialog-surface-dark};
}
}

View file

@ -10,4 +10,5 @@
@import "about-manifest";
@import "book";
@import "catalog";
@import "search";
@import "math-widget";

View file

@ -20,7 +20,15 @@
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;700&family=Instrument+Serif:ital@0;1&family=Inter:wght@300;400;500;600;700&display=swap"
rel="stylesheet"
/>
<link
href="{{ get_url(path='pagefind/pagefind-component-ui.css', trailing_slash=false) | safe }}"
rel="stylesheet"
/>
<link rel="stylesheet" href="{{ get_url(path='main.css', trailing_slash=false) | safe }}" />
<script
src="{{ get_url(path='pagefind/pagefind-component-ui.js', trailing_slash=false) | safe }}"
type="module"
></script>
{% block extra_head %}{% endblock extra_head %}
</head>
<body>
@ -30,6 +38,7 @@
"homeUrl": {{ get_url(path="/") | json_encode() | safe }},
"browseUrl": {{ config.extra.browse_url | json_encode() | safe }},
"aboutUrl": {{ config.extra.about_url | json_encode() | safe }},
"searchUrl": {{ config.extra.search_url | json_encode() | safe }},
"githubUrl": {{ config.extra.github_url | json_encode() | safe }}
}
</script>
@ -54,6 +63,7 @@
</footer>
<script type="module" src="{{ get_url(path='js/bundle.js', trailing_slash=false) | safe }}"></script>
<pagefind-modal reset-on-close></pagefind-modal>
{% block extra_scripts %}{% endblock extra_scripts %}
</body>
</html>

View file

@ -22,7 +22,7 @@
<span aria-current="page">{{ section.title }}</span>
</nav>
<article class="book-chapter">
<article class="book-chapter" data-pagefind-body>
<header class="book-chapter__header">
<div class="book-chapter__header-main">
<p class="section-kicker">Number &amp; Algebra</p>
@ -31,12 +31,12 @@
<p class="book-chapter__lead">{{ section.description }}</p>
{% endif %}
</div>
<div id="copy-page-button" class="book-chapter__actions"></div>
<div id="copy-page-button" class="book-chapter__actions" data-pagefind-ignore></div>
</header>
{% if section.extra.chapter_id == "quadratic-equations" %}
{% include "partials/math/quadratic-equations-content.html" %}
<div id="quadratic-explorer" class="math-widget-mount"></div>
<div id="quadratic-explorer" class="math-widget-mount" data-pagefind-ignore></div>
{% endif %}
</article>
</div>

View file

@ -93,9 +93,10 @@
<p class="section-kicker">Planet</p>
<h2 class="section-title">Open textbooks are gentler on the Earth</h2>
<p>
Core curriculum changes slowly. Newton’s laws, the structure of a cell, and how fractions work do not
need a new AI run every September. Regenerating the same chapter thousands of times burns electricity
in data centers — for text that could have been written once and shared openly.
Everybody learns the same basics. Everybody prompts AI the same thing. Core curriculum changes slowly.
Newton’s laws, the structure of a cell, and how fractions work do not need a new AI run every September.
Regenerating the same chapter thousands of times burns electricity in data centers — for text that could
have been written once and shared openly.
</p>
<p>
<strong>One carefully reviewed book</strong>, published on the web for everyone, means less redundant

View file

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block title %}{{ page.title }} | {{ config.extra.site_title }}{% endblock title %}
{% block meta_description %}
<meta name="description" content="Search Open Free Books chapters and site pages." />
{% endblock meta_description %}
{% block content %}
<section class="search-page">
<div class="container search-page__layout">
<p class="section-kicker">Search</p>
<h1 class="section-title search-page__title">{{ page.title }}</h1>
<p class="search-page__lead">Find chapters and pages across Open Free Books.</p>
<div class="search-page__ui">
<pagefind-input autofocus placeholder="Search books and chapters…"></pagefind-input>
<pagefind-summary></pagefind-summary>
<pagefind-results></pagefind-results>
<pagefind-keyboard-hints></pagefind-keyboard-hints>
</div>
</div>
</section>
{% endblock content %}

View file

@ -23,3 +23,4 @@ browse_url = "/catalog/"
about_url = "/about/"
credits_url = "/credits/"
contributing_url = "/contributing/"
search_url = "/search/"