Personal portfolio / about-me site, built with Next.js and deployed to Vercel.
- Next.js 16 (App Router, Turbopack)
- React 19
- TypeScript
- Tailwind CSS 4
- Motion for animations, Zustand for client state
- Headless UI + Heroicons for accessible UI primitives
- Node.js — see
.nvmrc(Node 24) - pnpm — see the
packageManagerfield inpackage.json
nvm use # or: nvm install
corepack enable # provisions the pinned pnpm versionpnpm installpnpm dev # start the dev server at http://localhost:3000pnpm build # production build
pnpm start # serve the production buildpnpm lint # ESLint + Prettier + tsc
pnpm format # PrettierTwo layers. Unit tests cover the pure helpers and sit next to the code they
test as *.test.ts under src/; they run on Node's own test runner,
no browser needed. Everything else is end-to-end with
Playwright: that suite lives in e2e/ and
always runs against the production build; see
playwright.config.ts for why.
pnpm exec playwright install chromium # once, to fetch the browser
pnpm test # unit tests, then boots `next start` and runs e2e
pnpm test:unit # unit tests only — fast, no browser
pnpm test:e2e # end-to-end onlyplaywright.config.ts starts and stops the server itself, so no separate
pnpm start is needed. It derives the port from this checkout's path, so
parallel worktrees never end up testing each other's build, and locally reuses
a server already listening on it. Set E2E_PORT to pin a port instead.
When a CI run fails it uploads a playwright-report artifact. Open it with
pnpm exec playwright show-report <unzipped-dir> — the report fetches its own
data over HTTP, so opening index.html from file:// shows an empty page.
-
Every dependency is pinned exactly — no
^or~. -
pnpm-lock.yamlis Prettier-formatted and lint-checked. A rawpnpm installrewrites it in pnpm's own compact style and turnspnpm lintred, so runpnpm exec prettier --write pnpm-lock.yamlafter any install that changes it. -
A 7-day supply-chain cooldown (
minimumReleaseAge) is pinned inpnpm-workspace.yaml: pnpm will not resolve a version published less than a week ago. Two traps worth knowing:- The unit is minutes, not seconds.
10080is 7 days; a seconds-shaped1209600is ~840 days and blocks essentially all of npm. - pnpm ships no default for it, so an unpinned repo silently inherits
whatever is in each contributor's global pnpm rc (
pnpm config listshows where a value came from). That is why it is pinned here rather than left off.
To pull something newer than the window — an urgent security patch, say — use
pnpm add <pkg> --config.minimumReleaseAge=0, or give a package a standing exemption viaminimumReleaseAgeExclude. CI installs with--frozen-lockfile, which skips resolution, so it is never affected. - The unit is minutes, not seconds.
Pushes to the default branch are deployed automatically by Vercel.