diff --git a/CLAUDE.md b/CLAUDE.md index 3852442..5b2a7f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,22 +25,37 @@ App lives in `apps/web`. Concept and scope: `apps/web/plans/freelance-relaunch.m - 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. -- One page: everything lives on the homepage. The header has only "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 `/imprint` and `/privacy`. URLs of the previous site redirect to `/` (see `next.config.mjs`). -- Title, description and link previews come from `SITE_TITLE` and `SITE_DESCRIPTION` in `src/content/site.ts`. - The share image is a screenshot of the real hero: after changing the hero headline or its design, run - `pnpm --filter @mintel/web share-image` and commit the two PNGs in `app/(site)/`. +- One page: everything lives on the homepage. The header has 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 (`/`, `/impressum`, `/datenschutz`), English under `/en` (`/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`. Reuse them instead of inventing new effects. + `BinaryTicker`, `ScrollBits`, `BitBurst`, `BitCurtain` (the language switch transition). Reuse them + instead of inventing new effects. - `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`). diff --git a/apps/web/app/(site)/imprint/page.tsx b/apps/web/app/(site)/imprint/page.tsx deleted file mode 100644 index d7cd2c1..0000000 --- a/apps/web/app/(site)/imprint/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import type { Metadata } from "next"; -import { EmailLink } from "@/src/components/EmailLink"; -import { LegalPage, LegalSection } from "@/src/components/LegalPage"; -import { OWNER } from "@/src/content/site"; - -export const metadata: Metadata = { - title: "Imprint", - description: "Legal notice (Impressum) for mintel.me.", - alternates: { canonical: "/imprint" }, - robots: { index: false, follow: true }, -}; - -const LINK = "font-bold text-slate-900 underline-offset-4 hover:underline"; - -export default function ImprintPage() { - return ( - - -

- {OWNER.name} -
- {OWNER.street} -
- {OWNER.city} -
- {OWNER.country} -

-
- - -

- Email: -

-
- - -

- VAT identification number according to section 27a of the German VAT - Act (Umsatzsteuergesetz): {OWNER.vatId} -

-
- - -

{OWNER.name}, address as above (section 18 paragraph 2 MStV).

-
- - -

- I am neither willing nor obliged to take part in dispute resolution - proceedings before a consumer arbitration board. -

-
-
- ); -} diff --git a/apps/web/app/(site)/layout.tsx b/apps/web/app/(site)/layout.tsx deleted file mode 100644 index 88fb17a..0000000 --- a/apps/web/app/(site)/layout.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import type { Metadata } from "next"; -import { Inter, JetBrains_Mono, Newsreader } from "next/font/google"; -import { BitBurst } from "@/src/components/bits/BitBurst"; -import { Footer } from "@/src/components/Footer"; -import { Header } from "@/src/components/Header"; -import { InteractiveElements } from "@/src/components/InteractiveElements"; -import { - SITE_DESCRIPTION, - SITE_NAME, - SITE_TITLE, - SITE_URL, -} from "@/src/content/site"; -import "./globals.css"; - -const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); -const jetbrainsMono = JetBrains_Mono({ - subsets: ["latin"], - variable: "--font-jetbrains-mono", - display: "swap", -}); -const newsreader = Newsreader({ - subsets: ["latin"], - variable: "--font-newsreader", - style: "italic", - display: "swap", -}); - -export const metadata: Metadata = { - metadataBase: new URL(SITE_URL), - title: { - default: SITE_TITLE, - template: `%s | ${SITE_NAME}`, - }, - description: SITE_DESCRIPTION, - applicationName: SITE_NAME, - authors: [{ name: SITE_NAME, url: SITE_URL }], - creator: SITE_NAME, - icons: { - icon: "/favicon.svg", - apple: "/apple-touch-icon.png", - }, - // The images come from opengraph-image.png and twitter-image.png next to this file. - openGraph: { - type: "website", - url: "/", - siteName: SITE_NAME, - locale: "en_US", - title: SITE_TITLE, - description: SITE_DESCRIPTION, - }, - twitter: { - card: "summary_large_image", - title: SITE_TITLE, - description: SITE_DESCRIPTION, - }, -}; - -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( - - -
-
{children}
-