Files
mintel.me/CLAUDE.md
T
mmintelandClaude Opus 5.5 3e0c9a1605 feat: a red thread through the homepage, in words and as a line
The rail and the "next, you are wondering" links are gone (owner: flat, and
the question stood twice in a row). The page now reads from top to bottom:

Every chapter opens with one sentence, and the sentences follow each other
as the way of a project: you need someone who builds it, first we find out
what you need, sometimes it is less work by hand, sometimes a meeting, then
it always runs the same way, in the end something runs, a few questions
remain, and it starts with an email.

One line runs down the whole page and draws itself as the visitor scrolls.
Under each sentence it crosses the page, stands a figure on the line
(diamond, hexagon, screen, circle, square, triangle) and continues down the
other side; a glowing dot travels at its head and comes to rest in front of
the last sentence.

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

7.7 KiB

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 line down the whole page that draws itself as the visitor scrolls: under each sentence it crosses the page, stands a figure on the line and continues on the other side, with a glowing dot 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).
  • 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.