diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index a36e28a..8fd5b40 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -17,6 +17,7 @@ permissions: jobs: verify: runs-on: ubuntu-latest + timeout-minutes: 15 defaults: run: working-directory: frontend @@ -41,3 +42,7 @@ jobs: run: npm run test:posthog - name: Build production app run: npm run build + - name: Install browser for production checks + run: node node_modules/playwright-core/cli.js install --with-deps chromium + - name: Verify production page and API boundaries + run: npm run test:browser diff --git a/README.md b/README.md index 73ff981..47be5f0 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,49 @@ # CodesWhat website -Private source for the public CodesWhat org site at codeswhat.com. The -Next.js app lives in `frontend/`; everything deployable is under that -directory. - -## What this repo is (and isn't) - -This is permanently private infrastructure, not a community project. That's -why there's no LICENSE, CONTRIBUTING, SECURITY policy, or CODEOWNERS here: -the site's source isn't published, only the rendered site is. The tier -decision and rationale live in the private ops repo -(`decisions/repository-tiers.md`). - -## Working here - -- Flow: feature branches PR into `dev`, then `dev` promotes to `main`. -- CI: `website.yml` runs the website contract checks. -- Deploys: Vercel, but git-integration deploys are broken for this repo (a - private repo in a GitHub org can't deploy on the Hobby plan). Production - deploys happen from a local checkout with - `npx vercel deploy --prod --scope codeswhat`. -- Analytics: PostHog via the shared "CodesWhat Public Websites" project, - proxied through `e.codeswhat.com`, cookieless. - -## More docs - -- `docs/README.md` - development guide -- `frontend/README-VERCEL.md` - deploy specifics -- `ROADMAP.md` - planned sections and site direction +Public source for [codeswhat.com](https://codeswhat.com), the CodesWhat project +and consulting site. The deployed Next.js application lives in `frontend/`. + +## Development + +Use Node.js 24. From `frontend/`, install the locked dependencies with `npm ci` +and start the development server with `npm run dev`. + +Existing checks are `npm run lint`, `npm run type-check`, `npm run test:scene` +and `npm run test:posthog`. Run `npm run build`, then `npm run test:browser` +to check the production page and invalid newsletter input. The browser check +uses the existing Playwright dependency with installed Chromium or Chrome; +CI installs its matching Chromium build. It uses a temporary local server +and blocks external requests. +The root Lefthook configuration runs staged Biome checks and typechecking at +commit time. Keep those checks enabled. + +Changes go through a feature PR into `dev`, followed by a reviewed promotion +from `dev` to `main`. Preserve the same tested content through promotion. +The `Website contracts` workflow checks pull requests and the integration and +production branches. + +## Deployment and verification + +Vercel's Git integration deploys `main` to production with `frontend/` as the +project root. Verify the deployment's source commit and the actual page after +promotion. A successful build or pageview event does not prove that the page's +client components initialized. + +The Content Security Policy uses a fresh request nonce for Next's generated +scripts and the initial theme script. HTML renders per request; immutable +JavaScript and static assets keep their existing caching. Keep the request and +response policies consistent, and verify a real theme toggle and mounted scene +when changing scripts, headers or rendering. + +Analytics uses the shared CodesWhat Public Websites PostHog project through +`e.codeswhat.com`, with the existing cookieless sanitizer. Preview and local +development should leave its production-only environment values unset. +Newsletter delivery uses server-side EmailOctopus credentials. See the +[deployment guide](frontend/README-VERCEL.md) for configuration. + +## Repository boundaries + +Internal plans and operational scratch belong in ignored `.planning/`, and +`.claude/` stays ignored to protect nested agent worktrees. Neither is a build input. +Don't commit credentials or local environment files. Existing public source +does not imply that private planning is safe to publish here. diff --git a/frontend/README-VERCEL.md b/frontend/README-VERCEL.md index 7c270d3..17e647c 100644 --- a/frontend/README-VERCEL.md +++ b/frontend/README-VERCEL.md @@ -1,27 +1,40 @@ # Vercel Deployment Guide -## Quick Deploy +## Production deployment -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/codeswhat/website) +The existing Vercel project deploys `CodesWhat/codeswhat.com` from `main`, with +`frontend/` as its root directory and Node.js 24. Feature changes merge into +`dev`, then a reviewed promotion advances `main`. Git integration is working; +a local CLI deployment is not the routine production path. -## Manual Deployment +After promotion, confirm the production deployment references the intended +commit. Open the canonical domain and verify the theme toggle changes theme +and the client scene mounts. Build success and analytics events alone do not +establish that client initialization worked. -1. Install Vercel CLI (optional): +## Rendering and security headers - ```bash - npm i -g vercel - ``` +The Next proxy sets a fresh script nonce on both the forwarded request policy +and the response policy. Next uses that nonce for generated inline scripts; +the root layout applies it to the theme-init script. HTML renders per request +so nonce values are not reused through static prerendering. Static JS/assets +retain the caching rules in `vercel.json`. -2. Deploy: - ```bash - vercel - ``` +Keep the CSP's allowed third-party origins narrow. A fixed hash for only the +theme script does not permit Next's inline Flight scripts and breaks hydration. +The production browser regression verifies actual interaction and blocked +unapproved scripts. The remaining response headers and API cache rules live +in `vercel.json`. ## Environment Variables Add these in your Vercel dashboard under Settings → Environment Variables: -### Required Variables +### Site and newsletter configuration + +The site metadata variables are optional overrides for the defaults in +`lib/site-config.ts`. EmailOctopus credentials are required for newsletter +delivery and stay server-side. | Variable | Description | Example | | ------------------------------ | ------------------------ | -------------------------------- | @@ -53,17 +66,17 @@ unset in Preview and Development so those deployments emit no analytics: The `vercel.json` file includes: -- ✅ Security headers -- ✅ API route configuration -- ✅ Function timeouts -- ✅ Caching rules +- Additional security headers +- API route configuration +- Function timeouts +- Static-asset caching rules ## Features Configured - **Security Headers**: XSS protection, frame options, content type sniffing prevention - **API Caching**: Disabled for `/api/*` routes - **Function Duration**: 10 seconds max for email signup -- **Sitemap**: Rewrite rule ready for dynamic sitemap +- **Sitemap**: Next's sitemap route supplies the public page list ## Monitoring @@ -81,7 +94,7 @@ The `vercel.json` file includes: ### Build Failures - Check build logs in Vercel dashboard -- Ensure Node.js version matches locally (20.9.0+) +- Ensure Node.js version matches locally (24.x) - Verify all dependencies are in package.json ### Email Signup Not Working diff --git a/frontend/app/api/subscribe/route.ts b/frontend/app/api/subscribe/route.ts index 0f57d09..307c2af 100644 --- a/frontend/app/api/subscribe/route.ts +++ b/frontend/app/api/subscribe/route.ts @@ -44,14 +44,18 @@ export async function POST(request: Request) { } // Parse request body - let body: { email?: string }; + let body: unknown; try { body = await request.json(); } catch { return NextResponse.json({ error: "Invalid request format" }, { status: 400 }); } - const { email } = body; + if (body === null || typeof body !== "object" || Array.isArray(body)) { + return NextResponse.json({ error: "Invalid request format" }, { status: 400 }); + } + + const email = "email" in body ? body.email : undefined; // Input validation if (!email || typeof email !== "string") { diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 04d6430..2d8844e 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { Inter } from "next/font/google"; +import { headers } from "next/headers"; import { ThemedToaster } from "@/components/site/ThemedToaster"; import { BASE_URL, OG_IMAGE, SITE_CONFIG } from "@/lib/site-config"; import { THEME_INIT_SCRIPT } from "@/lib/theme-init-script"; @@ -67,16 +68,21 @@ export const metadata: Metadata = { }, }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const nonce = (await headers()).get("x-nonce") ?? undefined; return ( -