AGENTS.md@apps/website · diff
git:20261019.69613f2 to git:20261020.8d47d7a
79 added, 226 removed. Audit A to A.
# Agent Instructions — `apps/website`
- The hosted cloud's front page at `/`, the preview's front door. It tells the
- story of `zitadel/new-website`'s narrative redesign (the branch
- `claude/zitadel-narrative-redesign-377a49`, its `docs/narrative-ia-proposal.md`):
- the benefit first, then its proof, and nothing else. Under a header to the
- docs and GitHub:
-
- 1. The hero: the new website's hero animation in the middle, "Your login in
- one minute." tucked into its faded bottom, one line on what it is, then the
- commands to copy, the skills line for coding agents, and the buttons to the
- quickstart and the repository, all of it on a laptop's first screen, with
- the top of the first step under it to tease what comes below, and from a
- laptop's width the steps' spine leading down to it from under the buttons.
- Short on purpose: no paragraph, no small print, no line above the headline; the
- header's Preview label frames the page, and the steps below say the rest.
- 2. The showcase (`app/showcase/`), with no title of its own: five numbered
- steps that prove the headline, what the commands do (start Zitadel, add the
- login) and what they let you change (a field, the flow, the brand).
-
- The footer is the logo alone. Feature lists, ways in and roadmaps belong on the
- docs and on zitadel.com, not here.
-
- Read together with the [root `AGENTS.md`](../../AGENTS.md) and
+ The hosted cloud's front page at `/`. Shared app rules:
[`apps/AGENTS.md`](../AGENTS.md).
- It is the seed of the public website, so it follows `zitadel/new-website`'s
- `apps/website`: Next.js with the App Router and Tailwind 4, and that repo's
- theme (`packages/theme`) for the look: its colours, APK Futural for headings,
- Arimo for text, Azeret Mono for code. `app/globals.css` keeps only the values
- this page uses, under the website's names (`bg-background`, `text-foreground`,
- `accent-strong`), so the website's components move over unchanged. It does not
- use `@zitadel/design-tokens`, the palette of the console, the login and the
- docs; the showcase's login previews are the one place their tokens paint, on
- the login's own atoms (below).
+ - The page says the benefit, then proves it, and nothing else: the hero with
+ the commands, then the showcase's steps. Feature lists, ways in and roadmaps
+ belong on the docs and zitadel.com.
+ - It ports `zitadel/new-website`'s narrative redesign
+ (`docs/narrative-ia-proposal.md` on that repo's
+ `claude/zitadel-narrative-redesign-377a49` branch) and follows that repo's
+ `apps/website` (Next.js App Router, Tailwind 4, its `packages/theme`), so
+ components move over unchanged. Keep the website's names in
+ `app/globals.css` (`bg-background`, `text-foreground`, `accent-strong`).
+ - It does not use `@zitadel/design-tokens`; only the login previews paint with
+ them, on the login's own atoms.
## The fonts
- All three faces load through `next/font/local` from `app/fonts/`, so the build
- fetches nothing. `next/font/google`, which `zitadel/new-website` still uses for
- Arimo and Azeret Mono, downloads them from Google at build time, and that
- download has failed builds: Google served a CI runner font URLs that Turbopack
- cannot parse, and a local build stalls when the network blips.
-
- - `Arimo-latin.woff2` is Arimo's variable font, every weight from 400 to 700,
- and `layout.tsx` declares it as such, as the docs do: the page sets text at
- 400 and 600, the login previews' card titles at 500.
- `AzeretMono-Medium-latin.woff2` is Azeret Mono at 500.
- - Each is the latin subset, byte for byte the file Google Fonts serves
- `next/font/google` (Arimo v36, Azeret Mono v21). A character outside it
- draws in the next face of the stack.
- - Both are under the SIL Open Font License 1.1, which has to travel with the
- files: `Arimo-OFL.txt` and `AzeretMono-OFL.txt` beside them, as Google Fonts
- publishes them. APK Futural is a licensed webfont, not an open one.
+ - Load every face through `next/font/local` from `app/fonts/`, never
+ `next/font/google`: its build-time download has failed builds.
+ - Each file is the latin subset Google Fonts serves `next/font/google`, byte
+ for byte. Keep each SIL OFL licence (`*-OFL.txt`) beside its file. APK
+ Futural is licensed, not open.
+ - To update one, take the file under `/* latin */` from the stylesheet served
+ to `next/font/google`'s user agent (Azeret Mono's family is
+ `Azeret+Mono:wght@500`):
- To update one, fetch the stylesheet with the user agent `next/font/google`
- sends and take the file under `/* latin */`:
+ ```sh
+ curl -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36' \
+ 'https://fonts.googleapis.com/css2?family=Arimo:wght@400..700&display=swap'
+ ```
- ```sh
- curl -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36' \
- 'https://fonts.googleapis.com/css2?family=Arimo:wght@400..700&display=swap'
- ```
+ - `apps/docs/app/fonts/` holds the same Arimo file; update both.
- Azeret Mono's is `family=Azeret+Mono:wght@500`. The docs keep a copy of the
- same Arimo file (`apps/docs/app/fonts/`); update both.
+ ## The hero animation
- ## The animation
+ - `app/hero.avif` is the new website's Lottie hero (`public/cms/homepage-hero-opt.json`
+ there, frames only, no vectors) as an animated AVIF;
+ `app/hero-still.webp`, its first frame, stands in for reduced motion and
+ browsers without AVIF. Rebuild it from the extracted frames:
- `app/hero.avif` is the website's homepage hero, the Lottie
- `public/cms/homepage-hero-opt.json` in `zitadel/new-website`. That Lottie holds
- no vectors, only 217 WebP frames (each asset's `p` is a base64 data URL), so it
- ships here as an animated AVIF: 49 KB instead of 907 KB, no player library, and
- the page's two copies of it, the animation and its blurred light, play in step.
- `app/hero-still.webp`, its first frame, stands in for reduced motion and for
- browsers without AVIF. It stands in the middle of the hero, above the
- headline, as large as the screen's height leaves room for beside the headline,
- the commands, the buttons and the top of the first step, between 15rem and
- 40rem wide (`.stage` in `app/globals.css`); when the hero's content changes
- height, change the 33.5rem it reserves for it. The hero takes the screen
- under the header but that top of the first step (`.hero`, `--peek`), its
- content centred in it, so a tall screen surrounds it with air and every
- screen from a laptop's up shows the first step's number, title and the top of
- its card at the fold, with room above them. In that air, from a laptop's width
- up, the steps' spine (`app/showcase/showcase.tsx`) begins: it fades in under
- the buttons (`.spine-teaser`), is drawn down once they are in, and runs on into
- the first step's number, however tall the screen. From the extracted frames:
+ ```sh
+ ffmpeg -framerate 30 -i %03d.webp -c:v libsvtav1 -crf 34 -preset 4 -pix_fmt yuv420p -f avif hero.avif
+ ```
- ```sh
- ffmpeg -framerate 30 -i %03d.webp -c:v libsvtav1 -crf 34 -preset 4 -pix_fmt yuv420p -f avif hero.avif
- ```
+ - When the hero's content changes height, change the `33.5rem` that `.stage`
+ (`app/globals.css`) reserves for it.
## The logo and the favicons
- `app/logo.tsx` is the website's animated logo, the Lottie
- `public/cms/zitadel-hover-alpha-opt.json`. Unlike the hero, it holds only
- vectors: the icon turns once, then the letters roll down one after another. It
- ships as inline SVG in the Lottie's own coordinates, its paths taken from the
- Lottie's shapes, and its motion as CSS keyframes in `app/globals.css` with the
- Lottie's timings and easings. As on the website it plays while the pointer is
- on it and returns to rest when the pointer leaves; here it also stays still
- for reduced motion. It sits in the header, the footer, where it is all there
- is, and the login previews' trustmarks, so it never plays on its own, where
- nothing hovers, as the centrepiece of the coming-soon page did. Its `id` keeps
- each copy's SVG ids apart. The docs' sidebar shows it too, from a copy
- (`apps/docs/components/logo.tsx`, its motion in `apps/docs/app/global.css`):
- change the copies together.
-
- `public/favicons/` holds the website's favicons,
- `packages/theme/assets/favicons` in `zitadel/new-website`, and
- `public/favicon.ico` its light one, laid out and declared (`app/layout.tsx`) as
- the website does: a dark mark for a light browser, a light one for a dark
- browser. They are the cloud's favicons, not only this page's: the console, the
- login UI and the docs link the same set at the same URLs, so a change here
- reaches them too; the storybook keeps Storybook's own. A single `/favicon.ico`
- cannot stand in for the set: each mark is one flat colour on a transparent
- ground, nearly invisible in the other theme's tab bar.
-
- `app/og.jpg`, the share image, is the website's own,
- `public/cms/og-zitadel-homepage.jpg` in `zitadel/new-website`: the logo on the
- brand's ground, nothing the preview would have to change. `layout.tsx` imports
- it, so it loads from `/_next/`, and sets no `metadataBase`: on Vercel, Next
- makes its URL absolute with the deployment's own host (`VERCEL_BRANCH_URL` on
- a preview, `VERCEL_PROJECT_PRODUCTION_URL` on production); a local build warns
- and uses `localhost`.
-
- ## The commands
-
- The hero shows the local preview as the commands of the published CLI
- (`@zitadel/cli@alpha`) in a terminal window (`app/command-block.tsx`), with
- Node 24, the CLI's `engines` floor, in its title bar, one button that copies
- them all and one per line; `/llms.txt` hands the same ones to agents. Both take
- them from `app/commands.ts`, with the skills line under them. They follow the
- README's quick start, and the docs' quickstart without its optional `doctor`: a
- new folder, `start`, `setup --framework next --server local`, `npm run dev`;
- the session the showcase was recorded from ran the same with `doctor` first.
- `--framework` keeps setup from asking; `--server local` needs no account.
- Never `--force`: if `mkdir` fails because the folder exists, the next lines run
- in the current directory, where `--force` would replace files. When that
- journey changes, change the commands, and the showcase with them, and run them
- once against the published CLI before they ship.
-
- ## The showcase
-
- `app/showcase/` holds the five steps under the hero (`showcase.tsx`,
- `row-layout.tsx`). From a laptop's width up (`lg`, 1024 px) they run along a
- spine, each numbered on it: the step's title and line on one side, sides
- alternating by step, and under them the terminal or the files and the result
- side by side, their tops on one line; a step without a result centres its
- terminal against its title instead. Narrower, they stack in one centred column
- as wide as a card reads well: title, terminal or files, result. Every
- window's title bar in it has one height (`TITLE_BAR` in
- `app/gradient-border.tsx`), so two windows side by side start their content on
- one line.
-
- They are `zitadel/new-website`'s narrative rows, ported: a command and what it
- printed (`terminal-card.tsx`), the card of the files setup writes
- (`file-tree.tsx`), the editor card with the diff, the flow's steps and the
- commands that publish the change (`file-card.tsx`), and the browser frame with
- a Before / After switch (`browser-frame.tsx`) around the login's page, drawn
- live (`login-preview.tsx`). A step's title is what you do, its line what
- happens; text between backticks in the line is code.
-
- - Steps 1 and 2 explain the hero's commands rather than repeat them: what
- `start` printed, the console link and the local server, and the files
- `setup` writes, the login, registration and profile pages, the proxy, and
- the user model and login flow that steps 3 and 4 edit, beside the login
- `npm run dev` serves. No replaying terminal: the narrative's, next to the
- hero's own commands, said the same thing twice.
- - Steps 1 to 4 come from a recorded session (`sessions.ts`), run with `script`
- on 5 October 2026 with @zitadel/cli 1.0.0-alpha.24: what its start printed,
- with the one-time console link masked, the files its setup wrote, its diffs
- and its command output. Re-record rather than edit when the CLI, what it
- prints, the files setup writes or the flow format changes.
- - The pages beside them are not pictures: `login-preview.tsx` draws each from
- the atoms of `@zitadel/components`, as the brand row does (below), as the
- screen `sessions.ts` names for it, the title, the fields, the action and the
- line under it, in the default template's English copy and with the fields
- the session's app served. So they are as sharp as the page around them and
- take the login's look on their own; when its copy or a step's fields change,
- change the screens.
- - In steps 3 and 4 the frame's switch shows the page before or after the
- change, and the fourth step's flow steps follow it: `identifier → password →
- done` before, `identifier + password → done` after, the password in the
- diff's green as what the identifier step took over, so the flow does not
- look like it lost it. Both screens stand in
- one place, the one not shown faded out and inert, so the frame keeps the
- taller one's height and the switch adds or takes away a field where it
- sits.
- - No captions or dates on the steps: this is a marketing page, the provenance
- lives here and in `sessions.ts`.
-
- ## The brand row
-
- The fifth step is live (`brand-row.tsx`): the visitor picks a brand colour, the
- corners and a heading face in the editor card's strip; the card's lines show
- the rule an app would set on `<zitadel-login>`, as the docs' "Customize the
- design" page themes it, and `login-preview.tsx` paints the result on the
- fourth step's result, the sign-in step as the flow's step leaves it (email and
- password), built from the atoms of `@zitadel/components` (`zl-card`,
- `zl-field`, `zl-button`) in their English copy. Before shows the card without
- the rule.
-
- - The atoms load, through one dynamic import for every preview, only once the
- first preview nears the viewport. Nothing submits: there is no server
- behind them.
- - An atom reads the `--zl-*` properties from its ancestors, and on its own
- brings none: `brand-tokens.ts` sets the base layer on the preview's wrapper
- from the package's own `tokens` and `cssVars` (every token at its dark
- value, under the name the generated stylesheet gives it), with the faces an
- app's login shows (Arimo, not the licensed APK Futural the heading token
- names first), and the visitor's choices over it.
- - The card's title, subtitle, field group, actions and register line are light
- DOM, styled in `login-preview.module.css` as the orchestrator's layout chrome
- styles them (`packages/components/src/orchestrator/templates/layout-chrome.css`).
- When that chrome, the default template or its English copy changes, follow
- it.
- - JSX for the atoms is in `app/zl-elements.d.ts`, typed from the atoms'
- classes; the package types only its orchestrators for React.
- - Next resolves the package to its `dist/`, so the website's build and dev
- tasks depend on `components:build` in moon and in turbo, and the Vercel
- service builds it through turbo before its own `pnpm run build` (Checks).
+ - `app/logo.tsx` is the website's animated logo as inline SVG, its motion as
+ CSS keyframes in `app/globals.css`. It plays only while hovered, never for
+ reduced motion; give each copy its own `id`.
+ - `apps/docs/components/logo.tsx` is a copy; change the copies together.
+ - `public/favicons/` and `public/favicon.ico` are the cloud's favicons: the
+ console, the login UI and the docs link the same set, so a change here
+ reaches them. A single `/favicon.ico` cannot replace the light/dark set.
+ - `layout.tsx` imports the share image (`app/og.jpg`) and sets no
+ `metadataBase`: on Vercel, Next makes its URL absolute with the deployment's
+ host.
- ## llms.txt
+ ## The commands and the showcase
- `app/llms.txt/route.ts` serves the host's `/llms.txt`: the hero's claim and
- what the preview is, the commands, the skills, and where an agent reads on,
- starting with the docs' own `/docs/llms.txt`. Its links are paths on the same
- host.
+ - `app/commands.ts` feeds the hero and `/llms.txt`. The commands follow the
+ README's quick start; never add `--force`. When that journey changes,
+ change the commands and the showcase, and run them once against the
+ published CLI.
+ - Steps 1 to 4 replay a recorded session (`app/showcase/sessions.ts`).
+ Re-record rather than edit when what the CLI prints, the files setup writes
+ or the flow format change.
+ - `login-preview.tsx` draws each login screen from `@zitadel/components`'
+ atoms in the default template's English copy. When that copy or a step's
+ fields change, change the screens.
+ - The previews' light-DOM chrome (`login-preview.module.css`) follows
+ `packages/components/src/orchestrator/templates/layout-chrome.css`; follow
+ its changes.
+ - The atoms load through one dynamic import once the first preview nears the
+ viewport. `brand-tokens.ts` sets their `--zl-*` base layer from the
+ package's `tokens` and `cssVars`; JSX types are in `app/zl-elements.d.ts`.
+ - Next resolves `@zitadel/components` to its `dist/`, so `build` and `dev`
+ depend on `components:build` in moon and in turbo.
## Routing
The cloud routes only `/`, `/llms.txt`, `/_next/*`, `/favicon.ico` and
- `/favicons/*` to this app (repo-root [`vercel.json`](../../vercel.json)); every
- other path belongs to another service, most of them to the server.
+ `/favicons/*` here (repo-root [`vercel.json`](../../vercel.json)).
- - A new page, or a file in `public/`, needs a rewrite of its own in
- `vercel.json`; without one the server answers its path with a 404.
- - Every other asset loads from `/_next/`: fonts through `next/font`, images as
- imports. Not through Next's metadata files (`app/icon.svg`,
- `app/favicon.ico`, `opengraph-image`): Next serves those at the root.
- - A link to another app is a plain `<a>`, never `next/link`: each is a
- deployment of its own, which this app must not prefetch or route to itself.
- `app/links.ts` holds them.
+ - A new page or `public/` file needs a rewrite of its own in `vercel.json`, or
+ the server answers it with a 404.
+ - Load every other asset from `/_next/`: fonts through `next/font`, images as
+ imports. Never use Next's metadata files (`app/icon.svg`, `opengraph-image`),
+ which Next serves at the root.
+ - Link another app with a plain `<a>` (`app/links.ts`), never `next/link`.
+ - `/llms.txt` (`app/llms.txt/route.ts`) links paths on the same host.
## Checks
- `moon run website:typecheck website:lint website:build`, and
- `moon run website:start` to look at the build on port 3004. On Vercel the
- service runs `turbo run build --filter=@zitadel/components`, then
- `pnpm run build`: the package's build goes through turbo and the team's remote
- cache, as the storybook's does, and the Next build itself never through a task
- runner, as the docs' does.
+ ```sh
+ moon run website:typecheck website:lint website:build
+ moon run website:start # the build on port 3004
+ ```
+
+ Vercel runs `turbo run build --filter=@zitadel/components`, then
+ `pnpm run build`; the Next build itself never goes through a task runner.