Skip to content

studio-website (sitecrate.ca)

studio-website is the public face of SiteCrate at sitecrate.ca. It is a single-page React 18 app that does three jobs: market the agency, capture prospect intake, and let clients track their project.

Hash-based routing — no React Router. All navigation goes through go(route) in App.jsx, which sets window.location.hash and calls trackPageView. The route is read from the hash on load and on every hashchange event.

Routes:

home | portfolio | how | pricing | start | contact | status/:token

status/:token is the client-facing project tracker. The token is a UUID stored in Supabase — not guessable. It reads via the get_project_status RPC, not direct table access.

The public site never touches the projects table directly. All anon access goes through SECURITY DEFINER RPCs keyed on the status_token capability, so the anon key can only ever see or affect the single project whose token is held.

SurfaceFileCalls
Intake formIntakePage.jsxsupabase.rpc('create_intake', { payload }) → returns the new status_token → fires send-email with { type: 'intake', token } (non-blocking)
Status page (read)StatusPage.jsxrpc('get_project_status', { p_token }) — client-safe columns only
Status page (write)StatusPage.jsxrpc('submit_project_feedback') / rpc('approve_project'), then fires send-email with { type: 'feedback' | 'approved', token }

See Capability-token RPCs & RLS for the full security model.

netlify/functions/send-email.js is the only email sender in the entire system — the admin app calls it too.

  • It resolves the project from the DB by token using the service-role key.
  • It builds all recipients and content server-side — never from the request body — so a forged request body can’t redirect or inject email.
  • CORS is locked to SiteCrate origins.
  • It sends via Resend from hello@sitecrate.ca.
  • Requires SUPABASE_SERVICE_ROLE_KEY + the Supabase URL in the function env.

Email types handled: intake, review, launch, feedback, approved.

GA4 property G-ZQDTTDS80V. All tracking flows through src/analytics.js — import track for preset events or trackEvent for custom ones. Page views fire on every route change via trackPageView.

Status routes are normalized: status/abc-123 is tracked as Project Status — the raw UUID is never sent to GA4 as a page title.

Tracked events: cta_click, generate_lead, pricing_click, portfolio_filter. See Analytics.

All styles live in src/styles.css. CSS custom properties in :root; the accent colour uses oklch(). No CSS modules, no Tailwind.

Breakpoints: 820px (nav), 720px (layout), 640px (status grid), 620px (form grid), 520px (footer/portfolio).

Terminal window
npm run dev # local dev server (Vite, port 5173)
npm run build # production build → dist/
npm run preview # serve dist/ locally
npm run lint # ESLint — must pass before merging any PR
netlify dev # local dev with Netlify functions (required to test send-email)