feat: rebuild mintel.me as a single-page freelance site

The site now presents one freelance developer on one page: hero, introduction,
four typical cases (developer for a product, make an app reliable, automate a
process, a website) with what you get, how it runs and what it costs, a
process automation section, technology and project fit, how I work, two live
references, questions and contact.

- Hero and closing headline are drawn from running binary digits.
- Each case explains its four steps with an animated drawing; the automation
  section shows example workflows with work travelling between systems; a
  pipeline run shows what a test catches before a release.
- Contact is one email address, kept in parts and assembled in the browser so
  it never appears in the page source (src/domain/email.ts, spec first).
- Imprint and privacy pages added.
- The old site (blog, case studies, contact form, fixed-price pages, video
  rendering and related scripts) is removed. Its URLs redirect to the homepage.
- The deploy smoke test checks the legal pages and the redirects instead of
  the removed case study.
- Dev scripts use docker compose v2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 22:33:06 +02:00
co-authored by Claude Opus 5.5
parent 4eb663f42a
commit fa40fefe2f
781 changed files with 4925 additions and 55137 deletions
+322
View File
@@ -0,0 +1,322 @@
/**
* Example workflows for the automation section. They are illustrations of typical
* cases, not descriptions of specific client projects.
*/
export type NodeKind = "trigger" | "rule" | "ai" | "action" | "human";
export type FlowNode = {
readonly id: string;
readonly kind: NodeKind;
/** Position of the node centre on the wide 920 x 400 canvas. */
readonly x: number;
readonly y: number;
/** Position of the node centre on the tall 400 x 540 canvas for phones. */
readonly nx: number;
readonly ny: number;
readonly system: string;
readonly label: string;
/** What appears in the log when a run reaches this node. */
readonly log: string;
};
/** One step of a run: the connections travelled at the same time. */
export type FlowStep = readonly (readonly [from: string, to: string])[];
export type Workflow = {
readonly id: string;
readonly name: string;
readonly summary: string;
readonly byHand: string;
readonly automated: string;
readonly nodes: readonly FlowNode[];
/** Every connection that exists, for drawing. */
readonly edges: readonly (readonly [from: string, to: string])[];
/** The runs that play in turn. Each run is a list of steps. */
readonly runs: readonly (readonly FlowStep[])[];
};
export const WORKFLOWS: readonly Workflow[] = [
{
id: "order",
name: "Order to invoice",
summary:
"A new order arrives in the shop. The invoice, the customer record and the confirmation follow without anyone typing.",
byHand:
"Someone opens the order, writes the invoice, updates the customer record and sends an email.",
automated:
"The order triggers everything. People only see the finished result.",
nodes: [
{
id: "shop",
kind: "trigger",
x: 110,
y: 200,
nx: 200,
ny: 60,
system: "Shop",
label: "New order",
log: "order received",
},
{
id: "stock",
kind: "rule",
x: 340,
y: 200,
nx: 200,
ny: 190,
system: "Rule",
label: "In stock?",
log: "stock checked: available",
},
{
id: "invoice",
kind: "action",
x: 580,
y: 110,
nx: 105,
ny: 330,
system: "Accounting",
label: "Create invoice",
log: "invoice created",
},
{
id: "crm",
kind: "action",
x: 580,
y: 290,
nx: 295,
ny: 330,
system: "CRM",
label: "Update customer",
log: "customer record updated",
},
{
id: "mail",
kind: "action",
x: 810,
y: 200,
nx: 200,
ny: 470,
system: "Email",
label: "Send confirmation",
log: "confirmation sent",
},
],
edges: [
["shop", "stock"],
["stock", "invoice"],
["stock", "crm"],
["invoice", "mail"],
["crm", "mail"],
],
runs: [
[
[["shop", "stock"]],
[
["stock", "invoice"],
["stock", "crm"],
],
[
["invoice", "mail"],
["crm", "mail"],
],
],
],
},
{
id: "documents",
name: "Incoming documents",
summary:
"Invoices arrive as PDF by email. An AI step reads them, a rule checks the result, and a person only sees the unclear ones.",
byHand:
"Someone opens every PDF, reads supplier, amount and date, and types them into the accounting system.",
automated:
"Clear cases are booked directly. Unclear ones go to a person with the document attached.",
nodes: [
{
id: "inbox",
kind: "trigger",
x: 110,
y: 200,
nx: 200,
ny: 60,
system: "Inbox",
label: "Email with PDF",
log: "document received",
},
{
id: "read",
kind: "ai",
x: 340,
y: 200,
nx: 200,
ny: 190,
system: "AI step",
label: "Read the document",
log: "supplier, amount and date extracted",
},
{
id: "check",
kind: "rule",
x: 570,
y: 200,
nx: 200,
ny: 320,
system: "Rule",
label: "Plausible?",
log: "values checked",
},
{
id: "book",
kind: "action",
x: 810,
y: 110,
nx: 105,
ny: 460,
system: "Accounting",
label: "Book it",
log: "booked automatically",
},
{
id: "ask",
kind: "human",
x: 810,
y: 290,
nx: 295,
ny: 460,
system: "A person",
label: "Review and decide",
log: "unclear: sent to a person for review",
},
],
edges: [
["inbox", "read"],
["read", "check"],
["check", "book"],
["check", "ask"],
],
runs: [
[[["inbox", "read"]], [["read", "check"]], [["check", "book"]]],
[[["inbox", "read"]], [["read", "check"]], [["check", "book"]]],
[[["inbox", "read"]], [["read", "check"]], [["check", "ask"]]],
],
},
{
id: "lead",
name: "Enquiry to follow-up",
summary:
"Someone fills in the contact form. The contact is created, the right person is told, and a reminder makes sure nobody forgets.",
byHand:
"Someone copies the enquiry into the CRM, forwards it by email and hopes the follow-up is not forgotten.",
automated: "The enquiry is filed, assigned and followed up on schedule.",
nodes: [
{
id: "form",
kind: "trigger",
x: 110,
y: 200,
nx: 200,
ny: 60,
system: "Website",
label: "Contact form",
log: "enquiry received",
},
{
id: "contact",
kind: "action",
x: 340,
y: 200,
nx: 200,
ny: 190,
system: "CRM",
label: "Create contact",
log: "contact created",
},
{
id: "notify",
kind: "action",
x: 580,
y: 110,
nx: 105,
ny: 330,
system: "Team chat",
label: "Notify sales",
log: "sales notified",
},
{
id: "wait",
kind: "rule",
x: 580,
y: 290,
nx: 295,
ny: 330,
system: "Rule",
label: "No reply in 3 days?",
log: "follow-up scheduled",
},
{
id: "remind",
kind: "action",
x: 810,
y: 290,
nx: 295,
ny: 470,
system: "Tasks",
label: "Create reminder",
log: "reminder created",
},
],
edges: [
["form", "contact"],
["contact", "notify"],
["contact", "wait"],
["wait", "remind"],
],
runs: [
[
[["form", "contact"]],
[
["contact", "notify"],
["contact", "wait"],
],
[["wait", "remind"]],
],
],
},
];
export const KIND_LABEL: Readonly<Record<NodeKind, string>> = {
trigger: "Trigger: something happens",
rule: "Rule: a fixed check",
ai: "AI step: reads or sorts",
action: "Action: a system does something",
human: "Person: decides the unclear cases",
};
export const AUTOMATION_NOTES = [
{
title: "Worth automating when",
items: [
"The same steps repeat every day or every week",
"Data is typed from one tool into another",
"Mistakes happen because the work is monotonous",
],
},
{
title: "What stays with people",
items: [
"Decisions and exceptions",
"Anything a customer should hear from a person",
"The final say when the automation is unsure",
],
},
{
title: "What I need from you",
items: [
"Access to the tools and their interfaces (APIs)",
"Someone who knows the process as it is done today",
"Real examples to test with",
],
},
] as const;