Bilingual (FR/EN) portfolio. Next.js 16, static export, no server.
npm install
cp .env.local.example .env.local # then fill in the Web3Forms key
npm run devnpm test # unit — Vitest
npm run typecheck # TypeScript
npm run lint # ESLint
npm run build # static export to out/
npx playwright test # a11y, reduced-motion, responsive gatesnpx playwright test serves the static export itself (npx serve out) on
port 4173, not 3000, and always starts its own server
(reuseExistingServer: false) rather than adopting one that happens to
already be listening. A stray next dev server left running on 3000 once
caused the whole quality suite — Playwright and both Lighthouse runs — to
silently measure a development build instead of the static export, with no
test failure to flag it. If you run Lighthouse by hand, point it at 4173 too
(npx serve out -l 4173, then http://localhost:4173/fr/).
- Write
content/case-studies/<slug>.fr.mdxand<slug>.en.mdxusing the six headings: Contexte, Contraintes, Stack, Décisions & arbitrages, Ce qui n'a pas marché, Résultat. - Add a loader entry in
lib/content/case-studies.tsand the slug tocaseStudySlugs. - Flip
hasCaseStudytotruefor that project inlib/content/projects.ts.
The test suite fails if those three fall out of sync.
Deployment is a manual step the site owner runs himself — it is not part of this repository's automation.
npx vercel --prodRun from the project root. Then, in the Vercel project's environment variables, set:
NEXT_PUBLIC_WEB3FORMS_KEYNEXT_PUBLIC_SITE_URL
Both are NEXT_PUBLIC_ variables, so they are baked into the static build at
build time rather than read at request time — setting them alone has no
effect on the already-deployed output. Redeploy after setting or changing
either one for the new value to take effect.
Two things about that redeploy, both of which have already caused a live site with a dead contact form:
- Enable each variable for every environment, not Production alone. A
branch deploy builds as Preview and inlines an empty string for anything
scoped to Production only. Neither variable is a secret —
NEXT_PUBLIC_values ship to every visitor in the JS bundle by design, which is how a static form works with no server — so there is nothing to protect by withholding them from Preview. - Uncheck "Use existing Build Cache" when redeploying after a change. The value lives inside an already-compiled chunk; a cached build restores that chunk and ignores the new variable entirely.
next build refuses to run at all when NEXT_PUBLIC_WEB3FORMS_KEY is empty
(see lib/env.ts), so a misconfigured deploy fails on the build host with an
explanation rather than shipping a form that rejects every message silently.
After deploying, check by hand on the live URL:
/redirects to/fr/- the FR↔EN switch preserves the current path
- the CV downloads in both locales
- the contact form delivers a real email
- both case studies render
- English CV.
public/cv/nomeny-mitia-andriamaheva-en.pdfis currently a byte-identical copy of the French CV. This is a deliberate placeholder so the download link is never broken, not a translation — the English CV is in French. - School name mismatch. The CV PDF names the school as "École Supérieure
de Management et Informatique Appliqué – Mahamasina", while the site's
dictionaries (
lib/i18n/dictionaries/fr.ts,lib/i18n/dictionaries/en.ts) say "ESMIA Innovation". The two disagree, and a visitor can open both from the same page (the Parcours section and the CV download).
No client names or client data in copy, screenshots, or commit messages.