The page was a row of sections to scroll past, and a film in one of them did not change that. It now follows the questions a visitor has, in the order they come up: who is this, can he do what I need, what does it look like, what else, how does it go, has he done it, what is open, how do I start. Each chapter ends by asking the next question as a large link that leads on, and the next chapter answers it. A rail at the edge shows every chapter as a station with a dot travelling along it (on phones: a pill with the current chapter and one tap to the next), and the header names the current question. Where the visitor is, is pure logic in src/domain/chapters.ts. The scroll film section and the bridge sentences are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
7.5 KiB
Markdown
106 lines
7.5 KiB
Markdown
# mintel.me
|
|
|
|
Personal website of a senior freelance engineer. Next.js 16 (App Router), pnpm monorepo.
|
|
App lives in `apps/web`. Social media videos live in `apps/video` (Remotion, standalone, own `CLAUDE.md`). Concept and scope: `apps/web/plans/freelance-relaunch.md` (read it first).
|
|
|
|
## Commands (run from repo root)
|
|
|
|
- `pnpm --filter @mintel/web typecheck` — must pass with zero errors
|
|
- `pnpm --filter @mintel/web test` — unit specs (Vitest); 0 tests run counts as a failure
|
|
- `pnpm --filter @mintel/web test:coverage` — `src/domain` must stay at 100%
|
|
- `pnpm --filter @mintel/web test:mutation` — Stryker on `src/domain`, ~2 min, must score 100
|
|
- `pnpm --filter @mintel/web lint`
|
|
|
|
## How to work here (spec first)
|
|
|
|
1. Write the behavior as concrete cases (inputs, outputs, edge cases, errors) as a `*.spec.ts`.
|
|
2. Run it and confirm it fails for the right reason (missing behavior, not an import error).
|
|
3. Implement the simplest clean solution, run again.
|
|
4. Run typecheck, coverage and mutation. Survivors mean weak specs or dead code: fix one or the other.
|
|
5. Report the exact commands and results. "Done" means they were run on the current code.
|
|
Never weaken, skip or delete a test to make it pass.
|
|
|
|
## Copy rules (client-facing pages)
|
|
|
|
- Never mention AI as the owner's tool, and no internal method vocabulary (specs, TDD, mutation, coverage,
|
|
pull requests). The automation service may name AI as part of what the client gets. Plain language.
|
|
- Modest and factual: no promises the owner did not confirm, no superlatives. See the concept, section 1.
|
|
- Two content pages. The homepage tells one story from top to bottom (who, what you need, examples, how
|
|
working together goes, results, questions, contact); each section opens with a `bridge` sentence from
|
|
`story` in the content files that leads over from the one before. The technical sections (project fit,
|
|
tools, how a change is secured) live on `/freelance` (`/en/freelance`), for people who develop themselves
|
|
or lead a team. Do not move technical detail back onto the homepage.
|
|
- The header has the link to the freelance page, the language switch and "Get in touch", which jumps to the
|
|
contact section (`#contact`). Contact is one email address: no calendar integration, no form. The only
|
|
other pages are the imprint and the privacy notice. URLs of the previous site redirect to `/`
|
|
(see `next.config.mjs`).
|
|
- Title, description and link previews come from `site` in `src/content/de.ts` and `en.ts`.
|
|
The share image is a screenshot of the real hero, one per language: after changing the hero headline or
|
|
its design, run `pnpm --filter @mintel/web share-image` and commit the PNGs in `src/assets/share/`.
|
|
- The email address is kept in parts (`src/content/site.ts`) and assembled in the browser by `EmailLink`.
|
|
Never write the full address into markup, metadata or copy.
|
|
- The hero design (binary-digit headline) is approved: do not change its design.
|
|
- Look at the result before reporting: take screenshots (desktop and mobile), do not ship blind.
|
|
|
|
## Two languages (German is the main one)
|
|
|
|
- German lives at the root (`/`, `/freelance`, `/impressum`, `/datenschutz`), English under `/en`
|
|
(`/en/freelance`, `/en/imprint`, `/en/privacy`). The app directory renders both under `app/[locale]`; `proxy.ts` maps the public addresses
|
|
onto it. All routing and detection rules are pure functions in `src/domain/routes.ts` and `locale.ts`.
|
|
- Detection: an address without a language follows the `lang` cookie (set only by the language switch), then
|
|
the browser's `Accept-Language`, then German. `/en/...` is never redirected.
|
|
- Every visible text is data in `src/content/de.ts` and `en.ts` (type `Content` in `types.ts`). Components
|
|
hold no texts: server components take them from `contentFor(locale)`, client components from
|
|
`useContent()`. A new text goes into both files; German uses the formal "Sie".
|
|
- Never name a file `src/i18n.ts` or `i18n/request.ts`: `@mintel/next-config` would switch on next-intl.
|
|
- The privacy notice describes the language cookie; keep it true when the detection changes.
|
|
|
|
## Motion (binary micro-interactions)
|
|
|
|
- Building blocks live in `apps/web/src/components/bits/`: `DecodeText` (mono labels resolving from digits),
|
|
`BitWipe` (headline revealed behind a column of digits), `BitReveal` (picture assembling from bits),
|
|
`BinaryTicker`, `ScrollBits`, `BitBurst`, `BitCurtain` (the language switch transition), `BitAura` (light
|
|
along the edge of an element, digits drifting off it; glow only on dark surfaces, on light ones just a
|
|
few blue digits), `BitSeam` (lit top edge of a dark section), `BitGlow` (light following the pointer on
|
|
`data-glow` surfaces). Reuse them instead of inventing new effects.
|
|
- The homepage is a guided story (`apps/web/src/components/story/`): it follows the questions a visitor has
|
|
(`story.chapters` in the content files). Each chapter ends with `NextChapter`, the next question as a link
|
|
that leads on; `StoryGuide` is the rail with one station per chapter and a travelling dot (a pill on
|
|
phones), and the header names the current question. Sections become chapters through `data-chapter`
|
|
(`Block` prop `chapter`). Where the visitor is, is computed in `src/domain/chapters.ts`. A new section
|
|
on the homepage needs a chapter and a question, or it breaks the thread.
|
|
- `DecodeText` is for monospaced text only; in proportional type the changing glyph widths make text jump.
|
|
- Never put an in-view trigger on an element that clips itself away (clip-path): a fully clipped element
|
|
never counts as visible. Observe an unclipped wrapper (see `BitWipe`).
|
|
- Every effect must respect `prefers-reduced-motion` and leave content readable without JavaScript.
|
|
|
|
## Architecture rules
|
|
|
|
- Functional core, imperative shell. Pure logic goes in `apps/web/src/domain/` (no I/O, no clock, no
|
|
randomness, no framework imports; pass `today` etc. in as arguments). Everything there is covered 100%
|
|
and mutation-tested. Pages, routes and adapters stay thin and call into the core.
|
|
- Parse at the boundary into precise types (`Result`, discriminated unions); no nullable state bags.
|
|
- No `any`, `@ts-ignore`, `!` non-null assertions or eslint-disable to silence errors. Fix the cause.
|
|
|
|
## Do not
|
|
|
|
- Read or edit `.env*` (only `.env.example`). Never print or log secrets.
|
|
- Commit scratch files (`tmp*`, `*.log`, `*.tmp`, debug HTML, build output). They are gitignored; keep it so.
|
|
- Use `--no-verify`, force-push, push to `main`, or run `reset --hard`, `clean`, `rm -rf` without asking.
|
|
- Edit `.gitea/workflows/*` or deploy config without asking.
|
|
- Upgrade `vitest` past 4.1.x: `@stryker-mutator/vitest-runner` 10.0.0 runs zero tests per mutant under
|
|
vitest 5 and reports false survivors. Re-verify mutation results after any tooling bump.
|
|
- Add dependencies for trivial things, or touch unrelated code (no drive-by refactors).
|
|
|
|
## Gotchas
|
|
|
|
- `tsconfig` includes `.next/types`: after deleting a route, stale generated files there break `tsc`.
|
|
Run `next typegen` and remove the stale file.
|
|
- Run only one Stryker process at a time; parallel runs share `.stryker-tmp` and hang.
|
|
- Stryker needs `plugins` listed explicitly in `stryker.config.json` (pnpm layout) and ignores the large
|
|
asset folders via `ignorePatterns`; keep them in sync when adding top-level folders.
|
|
- `@mintel/*` packages are `link:` dependencies on a sibling `at-mintel` checkout. The app itself now only
|
|
uses `@mintel/next-config`; the other entries in `package.json` are leftovers from the old site.
|
|
- Pushing `main` deploys to the testing environment; production is deployed from a `v*` tag.
|
|
- Git branches: work on `feat/*`, never directly on `main`.
|