Skip to content

Site review follow-up: security hardening + accessibility/UX pass#22

Open
GreerBK wants to merge 2 commits into
mainfrom
claude/zealous-hugle-2aaccb
Open

Site review follow-up: security hardening + accessibility/UX pass#22
GreerBK wants to merge 2 commits into
mainfrom
claude/zealous-hugle-2aaccb

Conversation

@GreerBK

@GreerBK GreerBK commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Why

Full site review (accessibility, UX, security) with fixes for everything actionable that turned up. Scores before this PR: Accessibility 8/10 · UX 7.5/10 · Security 6/10 — details below.

Security (the biggest gaps)

  • Airtable formula injection closed. User input (search text, filters) was interpolated into filterByFormula with a quote-doubling escape that Airtable doesn't actually document — a search term with a quote could break or alter the query. The API now sends Airtable one fixed query ({Status}='Active') and does all filtering in plain JavaScript. Bonus: search became case-insensitive ("yoga" now finds "Yoga").
  • Record-ID validation. /api/activity/<id> previously passed anything into the authenticated Airtable URL; encoded ../ could steer the request at other Airtable endpoints. IDs must now look like real record IDs, and non-Active records 404 (direct links can no longer surface Pending/Inactive rows).
  • Edge caching (~5 min) on all API functions — every visitor/filter combination is served from one cached Airtable payload, keeping load far under Airtable's 5 req/s limit (this was the easiest way to take the site down before).
  • Security headers via public/_headers: CSP, X-Frame-Options, nosniff, Referrer-Policy, Permissions-Policy, HSTS.
  • Dependencies: Vite 5 → 6.4.3; npm audit is now clean. Fonts self-hosted (no third-party requests), stray public/hello removed.

Accessibility (audience skews older, tremor, low vision)

  • Activity cards rewritten. They were <button aria-label="Name — view details">, which made screen readers skip the schedule, cost, address, and distance entirely. Now a real list of cards with a stretched name-link — everything is read aloud, and the link supports open-in-new-tab/copy-link.
  • Hero video pause button (WCAG 2.2.2 requires a way to stop auto-playing motion). The 8 MB video also now waits for the page to finish loading before it starts downloading, and skips entirely on data-saver connections.
  • Touch-target & text-size pass: filter rows 42px, chips 36px, distance ticks 40px, checkboxes 18px, action buttons 44px; the smallest text sizes raised site-wide.
  • Distance slider announces "50 miles" instead of bare "50"; scroll-to-top honors reduced motion; sidebar no longer blocks page scrolling when it has nothing to scroll.

UX

  • Keyword search box on the results page — the backend and state supported q but no input existed for it.
  • Share + Print buttons on activity pages, with a print stylesheet that turns a page into a clean handout (URLs printed out) — useful for pinning a schedule to the fridge.
  • A→Z sort when no location is given (was arbitrary Airtable order), "Try again" button on errors, live activity count on the home page, favicon + social-share preview tags, noscript message.
  • Removed the dead placeholder@example.com contact from the footer/error text — add a real contact email when one exists (TODO comment marks the spot in App.jsx).

Verified locally

Ran the real stack (wrangler pages dev + Airtable): injection attempts return clean 200s, traversal/garbage IDs 404, all headers present, keyword/zip/filter searches return correct live data (39 actives, "55101" → 30 within 50 mi sorted by distance), mobile layout has no overflow, console is clean, npm run build passes on Vite 6.

Worth considering later

  • Real URL routing (/activity/... instead of #/activity/...) so Google can index individual activities — needs care with existing shared links.
  • An Open Graph image (1200×630 PNG) so shares on Facebook show a card with a picture.
  • Compressing serene.mp4 (8.2 MB) — even deferred, it's heavy for rural connections.

🤖 Generated with Claude Code

GreerBK and others added 2 commits June 9, 2026 21:50
…on, edge caching, security headers

- /api/activities now sends Airtable a fixed query ({Status}='Active') and
  applies q/type/intensity/cost/format/days filtering in plain JS. The old
  quote-doubling escape was not a documented Airtable escape, so crafted
  search terms could break or alter the formula. Search is now also
  case-insensitive.
- /api/activity/[id] validates the record-ID shape before building the
  upstream URL (blocks encoded ../ traversal), returns 404 for non-Active
  records to match the list endpoint, and normalizes upstream errors to a
  generic 502.
- All three functions cache at the Cloudflare edge (~5 min), so every
  filter combination is served from one cached Airtable payload — keeps
  traffic far below Airtable's 5 req/s limit.
- public/_headers adds CSP, X-Frame-Options, nosniff, Referrer-Policy,
  Permissions-Policy (geolocation self-only), and HSTS.
- Remove stray public/hello test file; git-ignore .dev.vars.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Accessibility (the audience skews older, with tremor and low vision):
- Activity cards are now a real list of articles with a stretched name
  link — the old button+aria-label hid schedule/cost/distance from screen
  readers and nested divs inside a button (invalid HTML).
- Hero video gets a visible pause/play control (WCAG 2.2.2) and only
  mounts after window load so the 8 MB file never delays first paint;
  skipped under data-saver, hidden under reduced motion as before.
- Touch-target pass: filter rows 42px, checkboxes 18px, chips 36px,
  distance ticks 40px, share/print/back 44px; small UI text bumped from
  0.75–0.9rem to 0.85–1rem throughout.
- Distance slider announces "N miles" (aria-valuetext); scroll-to-top
  respects prefers-reduced-motion; sidebar no longer swallows mouse-wheel
  scrolling when it has nothing to scroll.

UX:
- Keyword search input on the results page (the q parameter existed in
  the API and state but had no UI).
- Results sort A→Z when there's no location to sort by distance.
- Share (native sheet or copy-link with live-region confirmation) and
  Print buttons on activity pages, plus a print stylesheet that turns a
  detail page into a clean paper handout with URLs spelled out.
- Error states get a Try again button; dead placeholder@example.com
  contact removed until a real address exists.
- Home page shows the live activity count; results heading reflects
  whether filters are applied.

Performance & polish:
- Fonts self-hosted via @fontsource (no Google Fonts request, simpler CSP).
- favicon, meta description, theme-color, Open Graph/Twitter tags, noscript.
- Home/search share one cached catalog fetch per session.
- Vite 5 → 6.4.3 clears all npm audit findings (dev-server esbuild advisory).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 10, 2026

Copy link
Copy Markdown

Deploying mnparkinsons with  Cloudflare Pages  Cloudflare Pages

Latest commit: 61fc2a3
Status: ✅  Deploy successful!
Preview URL: https://b03d25b1.mnparkinsons.pages.dev
Branch Preview URL: https://claude-zealous-hugle-2aaccb.mnparkinsons.pages.dev

View logs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant