Owner: the homepage is for people who need a website or process automation
and have no app of their own; companies that book by the hour belong on the
freelance page.
Homepage: two cases instead of three (automation, website). The price box
with its steps and figures is replaced by one box: you get an offer before
anything costs money. No rates or weekly prices there any more.
Freelance page: gets the developer case and "make an app reliable" with
their steps and prices, and the section on how working together goes.
Headlines the owner called unclear are rewritten: the automation section
("Your tools do the routine work. Nobody retypes anything.") and the
working section ("How working together goes. Direct and easy to follow.").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
117 lines
8.6 KiB
Markdown
117 lines
8.6 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 prices
|
|
and no hourly terms, only "ask for an offer". 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`.
|