Files
mintel.me/CLAUDE.md
T
mmintelandClaude Opus 5.5 af66f61c26 feat: care offer for websites, with its price and its limits
What happens after a website is live was one vague sentence about a monthly
fee to be agreed. It is now one clear offer in the website case and in the
questions: 89 EUR per month, cancel monthly, the site runs on servers in
Germany, small changes by email are included, no editing system and no login.
The limits are said as plainly as what is included: the first three months
are part of the project price, anything bigger gets a price first, replies
within two working days, no on-call, and the client can leave with everything.

The decision and the parts that are not on the website (the larger variant,
CMS as an exception, how the sites are served) are in the concept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:21:46 +02:00

118 lines
8.7 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 one story with a visible thread (`apps/web/src/components/story/`). Every chapter opens
with one sentence from `story` in the content files; read one after the other they tell the way of a
project from the idea to the first email. `StoryThread` is one trace down the whole page, in the look of a circuit
board (45 degree bends, solder points, a second parallel track), that draws itself as the visitor scrolls:
under each sentence it crosses the page, stands a component with the chapter number in binary on the track
and continues on the other side, with a bit (1/0) travelling at its head. `Block` prop `chapter` gives a section its sentence and
figure; how much of the line is drawn is decided in `src/domain/thread.ts`. A new section on the homepage
needs a sentence that continues the story, or it breaks the thread. No rails, no "next question" links
(owner rejected both).
- Drawings use the circuit parts in `apps/web/src/components/illustrations/circuit.tsx`: `Chip` (a system
or person, with pins), `Track` (straight runs, 45 degree bends), `Pad` (solder point), `Bit` (what travels).
New drawings are built from these, not from pills, curves and round dots.
- Two audiences, two pages (owner, 2026-10-05). The homepage is for people who commission a result and have
no software of their own: a website or process automation (plus web presentations). It shows no project prices
and no hourly terms, only "ask for an offer"; the one price there is the care offer for websites (89 EUR
per month, see the concept, section 5). Everything for companies that book by the hour is on the
freelance page only: the developer case, making an existing app reliable, how working together goes,
project fit, tools, safeguards, rates and booking. Do not put any of that back on the homepage.
- `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`.