Personal portfolio built with Next.js 16 (App Router) and React 19, deployed as a static export over FTP to OVH shared hosting.
- Next.js 16 + React 19 (React Compiler enabled)
- TypeScript (strict)
- Tailwind CSS v4 + shadcn/ui (Radix primitives)
- next-themes — dark mode
- next-intl — i18n (FR default + EN, routes under
app/[locale]) - Zustand — client state · nuqs — URL state · sonner — toasts
- React Flow + d3-force — interactive skills graph
- Biome — lint & format (replaces ESLint + Prettier)
- Vitest + Testing Library — unit/component tests
- Playwright — end-to-end tests
- Lefthook — git hooks
- @t3-oss/env-nextjs — type-safe environment variables
- Node.js 22+
- pnpm 11+ (pinned via
packageManagerinpackage.json)
pnpm install
cp .env.example .env.local # then fill in the values
pnpm devOpen http://localhost:3000.
| Command | Description |
|---|---|
pnpm dev |
Start the dev server |
pnpm build |
Production build |
pnpm start |
Serve the production build |
pnpm lint |
Biome check (lint + format) |
pnpm format |
Auto-format with Biome |
pnpm typecheck |
Type-check with tsc |
pnpm knip |
Find unused files, dependencies and exports |
pnpm test |
Run unit tests (Vitest) |
pnpm test:watch |
Vitest in watch mode |
pnpm test:e2e |
Run E2E tests (Playwright) |
First time running E2E tests:
pnpm exec playwright install chromium.
Environment variables are validated at build & runtime in src/env.ts.
Add new variables there and to .env.example. Browser-exposed
variables must be prefixed with NEXT_PUBLIC_.
src/
app/
[locale]/ localized routes (layout, page, …)
(site)/ public pages — (home), projets/[id], graph
sitemap.ts, robots.ts, [locale]/opengraph-image.tsx (non-localized / generated)
components/
shadcn-ui/ shadcn/ui primitives (via the CLI)
magic-ui/ Magic UI components (e.g. marquee)
controls/ reusable controls (theme-toggle, locale-switcher)
layout/ global site chrome (header/, footer/)
shared/ shared composed components (section, social-icons)
features/ per-domain modules: hero, about, skills, projects, trust, contact, graph
providers/ React context providers (composed in providers/index.tsx)
i18n/ next-intl config (routing, navigation, request)
messages/ translations (fr.json, en.json)
stores/ Zustand stores
styles/ global CSS (globals.css) + fonts (fonts.ts)
config/ static site config / metadata (site.ts)
lib/ framework-agnostic helpers (utils/cn.ts)
proxy.ts next-intl middleware (locale routing)
env.ts type-safe environment variables
tests/e2e/ Playwright end-to-end tests
A component is promoted to components/shared/ only when used in two or more places;
otherwise it is colocated with its route or owning feature. app/ stays routing-only.
/— home: hero (with a skills-graph preview), about, skills, featured projects, a "trusted by" marquee, and contact./projets+/projets/[id]— project list and detail (sharedfeatures/projectsdata)./graph— interactive skills graph (projects linked to the technologies they use), built with React Flow + d3-force.
Powered by next-intl. Supported locales: fr (default) and
en, with the locale in the URL (/fr, /en). Visiting / redirects to the detected
or default locale.
- UI strings live in
src/messages/fr.jsonandsrc/messages/en.json— add a key to both to add a string. - To add a locale: extend
localesinsrc/i18n/routing.tsand add a matchingmessages/<locale>.json. - Locale routing/detection lives in
src/proxy.ts(Next.js 16 renamedmiddleware→proxy).
Production is a static export hosted on OVH shared hosting, served over FTP — there is
no Node server in production. The build and upload live on the dedicated
static-export-ftp
branch, which sets output: "export" in next.config.ts so pnpm build
emits a fully static out/ directory. See DEPLOY-FTP.md on that branch for
the full procedure (WinSCP/FTP upload, .htaccess headers, root locale redirect).
- CI (
.github/workflows/ci.yml) runs lint, type-check, unit and E2E tests on every push and PR. - Deploy (
.github/workflows/deploy-ftp.ymlon thestatic-export-ftpbranch) builds the static export and uploadsout/to OVH via FTP on every push to that branch.
Because the export is static, the i18n middleware (src/proxy.ts) is dropped
in favour of static [locale] segments plus a root redirect, and security headers move from
next.config.ts to .htaccess. Set NEXT_PUBLIC_SITE_URL (build-time) to your production
domain so absolute URLs in the sitemap, robots and Open Graph tags are correct — it is read
from the SITE_URL repository secret in the deploy workflow.