← All projects

Case study · Search engine on Cloudflare

PAL Quick Search

An unofficial staff search tool over 112 Department of Education PAL policies — 98 of them deep-indexed at chapter level. Multi-word, typo-tolerant, hashtag-tagged. Live at pal.schooltool.com.au.

PAL Quick Search
Stack
Vite 5 · React 18 · Tailwind 3 · lucide-react · Cloudflare Pages
Status
Live in production, auto-deploy on push to main
Live
pal.schooltool.com.au · github.com/Vurctne/refined-pal-search
Role
Solo. Data extraction, deep-indexing pipeline, app design, build, deploy.
Audience
Victorian Government school staff (principals, business managers, teachers).
Dataset
112 PAL policies across 14 categories · 98 deep-indexed (chapter-level) · sourced from public DoE publications.

Problem

The Department of Education's PAL (Policy and Advisory Library) is the source of truth for "what's the rule about [almost anything] in a Victorian Government school". It's also painful to use: the official site is hierarchical (drill into a category, drill again, scan a list), search is keyword-only with no typo tolerance, and policies are presented top-level — to find a specific clause inside a 20-section finance manual you click in, scroll, ctrl-F, repeat.

Staff who need to look up "what's the policy on first aid for excursions", "who signs off on a stat dec", or "where's the reconciliation template for school council" routinely give up and call the principal, or just guess.

Solution

One page. Top of the screen: a search box with multi-word + typo-tolerant matching, a row of category filter chips (HR — Pay, HR — Leave, HR — Recruitment, Finance, School Council, Students, Child Safety, OHS, etc.), and a count of matches. Below: every matching policy as a card with name, summary, hashtag tags, category badge, "View on PAL ↗" external link to the official source, and a "Show details" expander that reveals chapters (numbered sections you can deep-link into), embedded resources (Word templates, DOCX procedures), and policy types (Policy and Guidelines / Guidance / Resources).

Type "first aid" — three matching cards appear instantly. Type "recon" — the Finance Manual surfaces, expanded shows 20 chapters and a deep-link to the reconciliation template DOCX. Click "School Council" — the two governance policies filter in. Hit a typo? Multi-word fuzzy matching catches it. Visit the page on a phone between classes — single-column, sticky search bar, large tap targets.

Built explicitly as an unofficial staff tool: every result links out to the canonical PAL source, and the disclaimer at the bottom makes the relationship clear.

Stack & why

Architecture

Single-component React app, single-route SPA, single static deploy. The interesting bit is the data pipeline that builds the bundled index.

public/
  _redirects               ← /* /index.html 200 (SPA fallback)

src/
  main.jsx                 ← mounts <PALSearch /> into #root
  PALSearch.jsx            ← the entire app: search, filter, render
  data/
    policies.json          ← 112 policies (built artifact)
    chapters.json          ← 98 deep-indexed policies' chapter lists
    suggested_queries.json ← "Try searching for" chips
    frequently_accessed.json ← featured policies (the starred ones)
  search/
    indexer.js             ← lunr-style precomputed index (multi-word + typo)
  index.css                ← @tailwind base/components/utilities

scripts/                   ← build-time pipeline (Python; not shipped to client)
  fetch_pal.py             ← scrape canonical PAL pages, validate against schema
  deep_index.py            ← parse chapter-level structure + extract resource links
  build_dataset.py         ← merge into the JSON files src/data/ imports

vite.config.js             ← @vitejs/plugin-react
tailwind.config.js
postcss.config.js
package.json               ← npm run dev / build / preview / deploy

The deploy script (npm run deploy) runs a local build then wrangler pages deploy dist for one-shot deploys. The default path is just git push origin main — Cloudflare Pages picks it up from the GitHub integration, runs the build, and ships the result to pal.schooltool.com.au in <90 seconds.

Hard parts

Deep-indexing 98 of 112 policies

Top-level matching ("does this policy mention X") was the easy half. Real value came from chapter-level indexing: the Finance Manual is one PAL entry but 20 sections deep — Section 7 is Chart of Accounts, Section 11 is Expenditure Management, Section 18 is End of Financial Year Reporting. Searching "recon" should find Section 21 (covering insurance + reconciliation), not just the manual top-page. The deep-index pipeline parses each policy's structure (when it has one), splits into chapters, indexes each separately, and surfaces the most-matched chapter when results expand. 98 of 112 policies have enough internal structure to deep-index; the remaining 14 are top-level only.

Multi-word search + typo tolerance without a backend

Browser-side fuzzy search over a few hundred indexable units sounds easy until you measure first-keystroke latency. Naive substring scan: ~80ms per keystroke on 200 entries with 14-category filter — perceptible lag. Switched to a precomputed inverted index (built at vite build time, shipped as JSON) plus a small fuzzy matcher that handles common typos and word reordering. Per-keystroke latency now <5ms on a mid-range phone.

"View on PAL" deep-links

Every result card links out to the canonical DoE PAL source — the whole point of the tool is to be a faster front door, not a replacement. The link extraction was finicky: PAL URLs vary in structure (some have stable slugs, some have query-param IDs, some are anchor-fragmented to chapter), and a wrong link silently sends staff to the homepage of the wrong section. The pipeline validates every URL at build time with a HEAD request and fails the build if any returns 404 or redirects to the PAL homepage.

Custom domain on a different DNS zone

pal.schooltool.com.au isn't in the same Cloudflare account as my brand domain. Setting it up needed a CNAME at the schooltool.com.au DNS provider pointing to the Pages *.pages.dev URL Cloudflare gave me, plus the custom domain entry on the Pages side. A few minutes for DNS to propagate and SSL to provision, then it just works. Documented in the repo README so the next person (or future me) doesn't have to retrace.

Result

What I'd do differently

I bundled the dataset (and the precomputed search index) into the JS payload — about 80KB gzipped for the data and 12KB for the index. That was the right call for 112 policies — first paint is the full data, no skeleton state. But it means a content update requires a full redeploy, and the JS bundle scales linearly with policy count. If I were starting over with the dataset 5× bigger, I'd put the policies in Cloudflare KV behind a tiny Workers function with edge caching, keep the UI exactly the same, and gain the ability to update content without a code deploy. For now the bundled approach is correct — but I'd watch the ~500-entry threshold.

Second thing: deep-indexing is currently a build-time pipeline. If the official PAL changes a chapter title or restructures a section, our index is stale until the next CI run. A scheduled Workers Cron Trigger that re-fetches and re-indexes weekly would catch most of these without human intervention. ~2 hours of work; high payoff for staff trust.

Screenshots

PAL Quick Search homepage — title bar with 'Unofficial staff tool · 112 policies · 98 deep-indexed · Multi-word search, typo tolerance', main search box, row of 14 category filter chips (HR - Pay, HR - Leave, etc.), Try searching for chip suggestions (long service leave, personal leave, parental leave, salary allowance, reconciliation, procurement, gift test, excursion form, working with children, hire or rehire, reporting incident, anaphylaxis, parent payments), Frequently accessed cards section with 8 starred policies, then a 112 policies count and the first three result cards (Allowances, Behaviour, Bullying Prevention) with hashtag tags and View on PAL links
Homepage on first paint. Full dataset visible, category chips for fast filtering, "Try searching for" suggestions, "Frequently accessed" cards for the most-hit policies, then the full 112 results. No spinner, no API call.
PAL Quick Search showing search query 'recon' with 2 results, the first being the Finance Manual expanded to show its 20 chapters list (Section 2 Governance through Section 18 End of Financial Year Reporting) plus a Resources card linking to the school council reconciliation template DOCX, Policy and Guidelines / Guidance / Resources type chips, and hashtag tags including #finance manual, #fmvgs, #cases21, #school finance
Deep-index in action. Query "recon" finds the Finance Manual; expanding shows its 20 numbered chapters and the embedded reconciliation template — the actual answer, two clicks deep instead of seven.
PAL Quick Search with the School Council category chip selected, showing 2 policies in School Council — School Council Meetings (with 8 sample agenda and minutes templates) and School Council Powers and Functions (mentioning From 2026, most council members require a WWCC), each with hashtag tags and View on PAL links, plus a disclaimer at the bottom 'This is an unofficial staff tool. Always refer to the official PAL for the authoritative version of any policy' and a contact line
Category filter — School Council surfaces just two policies with their hashtag tags and "View on PAL" deep-links. Disclaimer at bottom keeps the unofficial-tool relationship clear.
PAL Quick Search with search query 'leave' showing 15 results including Personal Leave, Annual Leave, Leave Without Pay, Recognition of Prior Service for Leave Purposes, and Study Leave — each result has the matched word highlighted in yellow throughout the snippet text and hashtag tags, plus expanded chapters and resource links
Search highlighting. Matched terms (here: "leave") highlighted in yellow throughout titles, summaries, chapter listings, hashtag tags. 15 results across the leave-related policies.

Try it

Live demo: pal.schooltool.com.au — opens instantly, no login, no install. Try searching "first aid" for a quick filter, "recon" to see deep-indexing find the right chapter, or filter by "School Council" to see category-only browsing.