apps/site — Operational Reference¶
Reference data for apps/site. Behavior rules and coding conventions live in apps/site/CLAUDE.md.
Stack¶
| Item | Value |
|---|---|
| Framework | React 19.2 + Vite 8 (Rolldown) |
| Language | TypeScript 6.0 (strict) |
| Styling | Tailwind CSS 4.1 + @kwilo/ui tokens |
| Routing | React Router 7 |
| SEO meta | react-helmet-async |
| Icons | @heroicons/react/24/outline |
| Forms | react-hook-form + zod (signup, demo request only) |
| Animation | framer-motion |
| i18n | react-i18next (common namespace only) |
| Rendering | Vite SSR (build-time) + SSG via scripts/prerender.mjs |
Dev Commands¶
pnpm dev # Express + Vite SSR middleware on port 5174 (HMR + real React rendering each request)
pnpm build # build:client → build:server → build:prerender
# dist/client/ — static SPA + per-route prerendered index.html
# dist/server/ — SSR bundle (build-time only, NOT deployed)
pnpm type-check # tsc --noEmit
pnpm preview # Run server.js in production mode against dist/client (local prod test)
Route Enumeration¶
src/App.tsx # Public routes
src/constants/routes.ts # ROUTES constants
src/pages/
├── Landing.tsx # / → current B2B landing
├── Individuals.tsx # /individuals → B2C landing
├── Pricing.tsx # /pricing → pricing tiers
├── Privacy.tsx # /privacy-policy → legal
└── AppRedirect.tsx # /login, /register, /dashboard → redirects to app.kwilo.ai
Cross-App URL Config¶
// src/config/urls.ts provides env-aware helpers
import { APP_LOGIN_URL, APP_REGISTER_URL } from '@/config/urls'
Dev default: http://app.kwilo.local:2015. Prod default: https://app.kwilo.ai. Override via VITE_APP_URL in .env.local.
SSR + SSG Architecture¶
Spec: docs/features/apps-site-ssg-migration.md. Issue: #653.
Three-File Entry Split¶
src/
├── main.tsx # Environment-agnostic. App + Router + providers + Helmet provider.
├── entry-client.tsx # hydrateRoot(getElementById('root'), <App />) — browser only
└── entry-server.tsx # export render(url) → renderToString(<App />) — server only
Build Pipeline¶
build:client—vite build --outDir dist/clientproduces the SPA bundle.build:server—vite build --ssr src/entry-server.tsx --outDir dist/serverproduces the SSR bundle.build:prerender—node scripts/prerender.mjsimportsrender, calls it per route, writesdist/client/<route>/index.html,robots.txt, andsitemap.xml.
Production: Cloudflare Pages serves dist/client/. dist/server/ is build-time only and never deployed.
Per-Route SEO Meta¶
Each route component declares its own <Helmet> block (title, description, canonical, OG/Twitter, JSON-LD). entry-server.tsx collects helmet output via helmetContext; the prerender script writes it into <!--app-head-->. No hand-crafted SEO HTML in prerender.mjs.
Adding a New Route¶
- Create the page in
src/pages/. - Add the route to
src/main.tsxusingROUTES.*. - Declare meta in the page component via
<Helmet>. - Add the route path to
scripts/prerender.mjs. - Run
pnpm build— verifydist/client/<route>/index.htmlhas correct meta tags. - Add a
Cache-Controlline for the route topublic/_headers.
SSR Incompatibilities¶
renderToString runs in Node — window, document, localStorage, IntersectionObserver don't exist.
- Don't access browser globals at module top-level or in render bodies.
- Use useEffect for DOM access after mount.
- Gate Vite-native paths with if (!import.meta.env.SSR).
Edge Caching¶
public/_headers: s-maxage=3600, stale-while-revalidate=86400 for prerendered routes. /images/* and /logos/* get 1-day browser cache. Hashed /assets/* are immutable. HTML always revalidates (max-age=0).
Deployment¶
- CF Pages project:
kwilo-site - Custom domains:
kwilo.ai(main branch),staging.kwilo.ai(staging branch) - CI workflow:
.github/workflows/site-cf-pages.yml - Full setup:
docs/infrastructure/cloudflare-setup.md - Local dev with subdomain split: docs/local-dev-subdomain-split.md