City of Ottawa drop-in recreation schedule data: a scraper, a protobuf dataset served at data.ottrec.ca, and the ottrec.ca website. Most work happens in the website.
This top-level directory is its own git repo (github.com/ottrec/.github), but
it tracks only base files (README, CONTRIBUTING, CLAUDE.md, .gitignore,
.github) — everything else below is gitignored. Each subfolder is a separate
repo or worktree of its own, tied together by go.work. So a git command at
the root touches the .github repo, not the subprojects; cd into the relevant
subfolder for its history.
website/— modulegithub.com/ottrec/website: the ottrec.ca site + the data.ottrec.ca data/API server. Most changes go here.scraper/— modulegithub.com/ottrec/scraper: the scraper (scraper/subdir) and the protobuf data schema (schema/).misc/,scripts/,infra/— supporting tooling and deployment.data/+cache/— the dataset repo (a--filter=blob:noneclone with a worktree); never regenerate or commit here.data/data.pb— a sample data protobuf for local testing without a live data server.
Make changes in the relevant worktree and run Go commands from that module's directory.
- The user is an experienced developer with deep domain knowledge of this project and its stack. Be concise and to-the-point; don't overexplain basics, restate the obvious, or pad with caveats and hand-holding. Lead with the answer or the change. Do call out problems you anticipate and alternative approaches worth considering.
- Subtle, muted UI. Default to quiet flexoki tints, bordered (not solid accent) buttons, small grey secondary text. Past corrections: "too prominent", "bold and distracting", "cluttered". Confirm before styling anything attention-grabbing.
- Dry, concise comments. Simple, to-the-point, concise, matter-of-fact, no cheesy/obvious phrasing, no em/en dashes (in copy or Go doc comments).
- Don't run the dev server or data-loading programs — the user runs those. Verify with build/generate/vet (see below), not by serving.
- The user iterates in rapid small follow-ups. Messages arriving mid-task are usually amendments; keep changes minimal and targeted.
- Scope changes to the website worktree unless told otherwise. In particular, don't touch the data.ottrec.ca rendering path unless explicitly asked; when shared code affects it, preserve its existing behavior.
- Never touch the scraper repo (
scraper/, modulegithub.com/ottrec/scraper). The one exception: if you genuinely need a helper on the schema types (scraper/schema/), ask the user first — don't add it unprompted. - Don't modify packages under
website/pkg/(ottrecidx,ottrecql,ottrecexp,ottrecexph,ottregions, etc.) unless explicitly asked, with clear instructions on the intended scope, edge cases, and semantics. These are load-bearing libraries with subtle behavior; treat them as consume-only by default. If a task seems to need a change there, surface it and ask rather than editing — and never change observable semantics as a side effect. - Modern TypeScript only: no frameworks, no jQuery-isms. ASI style (no statement-terminating semicolons). Use modern JS/TS syntax and APIs freely, as long as it transpiles/runs down to the supported targets (~Chrome/Firefox 110, Safari 15) — esbuild handles syntax, but mind runtime API availability on those browsers.
- Use modern Go features (the workspace is on a current Go toolchain): range-
over-func iterators,
min/max, generics,slices/maps/cmp, structuredlog/slog, etc. Prefer them over older hand-rolled equivalents. - For long-form text (docs, copy, posts, READMEs), don't write it out unprompted — give the user a short point-form list of recommendations on what may be worth adding and let them write it.
- The user stages changes and creates commits, not you. Don't
git addorgit commitunless explicitly asked. Changes made mostly by Claude get aCo-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>trailer in the commit message.
Adding/changing a page generally touches four things:
- Route — register in
routes.Website()(routes/website.go). Handlers embedwebsiteHandlerBaseand callh.render(w, r, func(data ottrecidx.DataRef) (templ.Component, int, error) {...}), which handles ETags, content-encoding negotiation, and error pages. New indexable pages also go in the sitemap path list. - Templates — templ files in
templates/. Regenerate withgo generate ./templates(notgo tool templdirectly). Pages share a navbar, theme head, and footer; reuse existing shared components rather than duplicating. - Static assets — files under
static/, registered instatic/static.goand referenced viastatic.Path(...)(content-hashed URLs). See the asset pipeline below. - Data access — everything reads through
pkg/ottrecidxDataRefgetters and iterators. Coordinates:fac.GetLngLat()returns lng, lat, ok (easy to swap). Timezone isottrecidx.TZ; dataset timestamp isdata.Index().Updated().
Notable packages: pkg/ottrecidx (indexed read API over the dataset),
pkg/ottrecql (the schedule query language used by search), pkg/ottrecexp(h)
(simplified export / HTML rendering), pkg/ottregions (lat/lng → Ottawa
region + sector, generated from OSM), internal/asset (the asset pipeline).
The site is server-rendered (templ) and works without JavaScript. Client-side TypeScript only progressively enhances an already-functional page — it never renders core content or gates it behind JS.
- Render the full, usable page on the server; links, forms, and content must work with JS disabled.
- Scripts attach to server-rendered markup and degrade gracefully: each enhancement no-ops if its target elements are absent, and there's a sensible no-JS fallback (e.g. a plain form/link behind an enhanced control).
- Pass data to scripts via server-rendered markup (data attributes / embedded JSON islands), not by fetching and rendering on the client.
- Prefer this over client-side rendering, SPA patterns, or frameworks. Reach for client JS only when the behavior genuinely can't be done server-side.
The pipeline lives in internal/asset; static/static.go wires it up. Assets
are content-addressed (hashed names) and compiled lazily at runtime, so
go build does not catch CSS/TS compile errors.
- CSS is minified by lightningcss (
compileCSS), withurl()references rewritten to hashed asset names. Targets ~Chrome/Firefox 110, Safari 15. - TypeScript is bundled to a minified IIFE by esbuild (
internal/esbuild, pure Go)..tsfiles can import each other and vendored npm packages (tree-shaken). tsc is editor/CI only — the server never type-checks. - npm deps are vendored as uncompressed tars under
static/lib/viago generate ./static(internal/npm); tracked inpackage.json. Type checking needsnpm installfirst (node_modulesis gitignored). - Fonts (Source Sans 3 / Source Serif 4 / Asap / Material Symbols) are
subset by explicit codepoint in
static/static.go. Material Symbols render via CSS::beforecontent escapes (ligature names don't work in the subset). - Theming is a flexoki light/dark palette driven entirely by
color-scheme+light-dark()CSS vars, with an anti-FOUC inline script.
Run from the website/ module dir:
go generate ./templates # after editing any .templ (check output for errors!)
go build ./... && go vet ./...
go run ./cmd/ottrec-website-check # compile all CSS + TS (server-free; catches what go build can't)
npx -y -p typescript tsc -p tsconfig.json # TS type checking (run npm install first)cmd/ottrec-website-check builds every static asset and reports compile
failures without serving; --css / --js / --all narrow what it checks. It
does not type-check TS. The commands live in cmd/: ottrec-website
(site), ottrec-data (data/API), ottrec-timemachine (experimental history
viewer), ottrec-website-check.
- A stale generated templ file still
go builds — always checkgo generate ./templatesoutput, not just the build. - In
.templtext, a run that begins withfor/if/switch(or a bare@) is parsed as a Go statement, not literal text. Reword (e.g. avoid starting prose right after a closing tag with those words) or escape as{ "for" }. routes.render's handler prologue expectsVary: Accept-Encoding(templates render panics without it). Follow an existing handler.
The scraper (scraper/scraper/main.go) scrapes City of Ottawa drop-in schedules
into a protobuf dataset. Design philosophy (see scraper/README.md): scrape
only basic facility + schedule info with minimal processing to keep the
scraper reliable and the schema stable long-term; freeform fields (notifications,
schedule changes, exceptions) are kept as best-effort raw HTML; parsing
problems become per-facility error messages rather than failures.
The schema is scraper/schema/schema.proto (schema.pb.go generated;
schema.go for helpers). It carries:
- Raw scraped fields (the source of truth, least lossy), plus
- Optional best-effort parsed/normalized fields (geocoded lng/lat, normalized activity/schedule names, parsed date/time ranges, reservation- requirement guesses). Parsing is unambiguous-only; overlapping/holiday schedules are not merged.
The dataset is published in raw (protobuf/textpb/json) and simplified
(json/csv) forms at data.ottrec.ca, updated daily. The website consumes it via
pkg/ottrecidx (raw protobuf), never re-scraping.
- Coordinates are lng, lat (
fac.GetLngLat()returns lng, lat, ok). Easy to swap; this exact bug shipped once historically. - Parsed/normalized fields are best-effort and optional. Parsed date/time ranges, normalized names, and reservation guesses may be absent or wrong when the source was ambiguous — always have a fallback (e.g. raw label/caption) and check the "ok" return on parsed getters.
- Schedule date ranges are negative-only. Overlapping/holiday schedules are not merged, so a schedule's start/end range only reliably tells you an activity does not apply on a date — not that it does. Don't compute "what's on now" from a single schedule's range alone.
- Fixed-date (holiday) schedules can override regular ones for specific dates and may share weekly times across different date ranges; treat them separately from recurring schedules.
- Freeform fields are raw HTML. Notifications, schedule changes, and exceptions are unparsed best-effort HTML — render/sanitize accordingly, don't expect structure.
- Per-facility errors exist and are meaningful. Each facility carries an errors array for parsing problems; surface them rather than assuming clean data.
- Names are normalized lowercase in the parsed fields; the human-facing label/caption is separate.
- The schema may change in backwards-incompatible ways; the raw protobuf is the least-lossy source of truth, the simplified export is the easiest to consume.