Skip to content

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

  1. build:clientvite build --outDir dist/client produces the SPA bundle.
  2. build:servervite build --ssr src/entry-server.tsx --outDir dist/server produces the SSR bundle.
  3. build:prerendernode scripts/prerender.mjs imports render, calls it per route, writes dist/client/<route>/index.html, robots.txt, and sitemap.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

  1. Create the page in src/pages/.
  2. Add the route to src/main.tsx using ROUTES.*.
  3. Declare meta in the page component via <Helmet>.
  4. Add the route path to scripts/prerender.mjs.
  5. Run pnpm build — verify dist/client/<route>/index.html has correct meta tags.
  6. Add a Cache-Control line for the route to public/_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