RuleStack

Configs

Stacks

Compare

Diff

RuleStack

Configs

Stacks

Compare

Diff

Read API

RuleStack

Configs

Stacks

Compare

Diff

Read API

Configs/TryGhost/Ghost/diff

Two files, one repository

TryGhost/Ghost ships 2 formats across 4 indexed files. The question worth asking is whether the second one says anything the first does not.

CompareAGENTS.md ↔ CLAUDE.md
A · AGENTS.md · 2464 wordsB · apps/shade/AGENTS.md · 1668 words
What each file covers, counted
DimensionSharedOnly in AOnly in BOverlap
Sections058190%
Commands33757%
Section tags86250%

What each file covers

Sections

0 shared · 58 only in A · 19 only in B
  • − AGENTS.md
  • − Package Manager
  • − Monorepo Structure
  • − ghost/* - Core Ghost packages
  • − apps/* - React-based UI applications
  • − koenig/* - Ghost editor (Koenig) packages
  • − packages/* - Shared workspace libraries
  • − e2e/ - End-to-end tests
  • − Common Commands
  • − Development
  • − Building
  • − Testing
  • − Unit tests (from root)
  • − Ghost core tests (from ghost/core/)
  • − These run on sqlite with no extra services. The Redis/MinIO/S3 adapter suites
  • − probe for their service and auto-skip when it's down (run `pnpm dev:storage`
  • − etc. to exercise them); they always run in CI, which starts the services.
  • − E2E browser tests (from root)
  • − Running a single test
  • − Watch a single DB-backed file (integration/e2e) — the default test:watch only
  • − covers unit tests, so point it at the DB config explicitly:
  • − Ember Admin tests (from the repository root)
  • − Run one Ember Admin test file. Paths are relative to apps/ember-admin.
  • − The explicit `1` supplies the numeric value required by the test script's
  • − trailing `--parallel` option before additional Ember Exam arguments.
  • − Linting
  • − Database
  • − Docker
  • − How `pnpm dev` works
  • − Start Ghost backend, Admin, Portal, and Docker services
  • − Add optional public apps (comments-ui, sodo-search, signup-form, admin-toolbar)
  • − Develop the Koenig editor against Ghost Admin (adds a koenig-lexical rebuild
  • − watcher + preview server; Admin loads the editor from your local build)
  • − With optional services (uses Docker Compose file composition)
  • − Everything available
  • − Architecture Patterns
  • − Admin Apps Integration (Micro-Frontend)
  • − Public Apps Integration
  • − i18n Architecture
  • − Build Dependencies (Nx)
  • − CSS Architecture
  • − TailwindCSS v4 Setup
  • − Entry Point
  • − Shade Styles
  • − Critical Rule: Embedded Apps Must NOT Import Shade Independently
  • − Public Apps
  • − Legacy Apps
  • − Code Guidelines
  • − Commit Messages
  • − ESLint Config
  • − When Working on Admin UI
  • − When Working on Public UI
  • − When Working on Backend
  • − Design System Usage
  • − Analytics (Tinybird)
  • − Troubleshooting
  • − Build Issues
  • − Test Issues
  • + Shade — Agent guide
  • + Core assumptions
  • + The five layers
  • + Decision flow: where does new code go?
  • + When to ADD to Shade vs keep local
  • + Conventions
  • + File names
  • + Component file structure
  • + Storybook titles
  • + Tokens & dark mode
  • + Required states for components
  • + ShadCN guardrails
  • + Build, test, dev
  • + Testing expectations
  • + Anti-patterns (don't do these)
  • + Commit & PR conventions
  • + Acceptance checklist (component)
  • + Repo layout
  • + Human docs

Commands

3 shared · 37 only in A · 5 only in B
  • − pnpm run setup
  • − pnpm dev
  • − pnpm build:clean
  • − pnpm test:watch
  • − pnpm test:integration
  • − pnpm test:e2e
  • − pnpm test:all
  • − pnpm test:single test/unit/path/to/test.test.js
  • − pnpm exec vitest -c vitest.config.db.ts test/integration/path/to/test.test.js
  • − pnpm nx run ghost-admin:test
  • − pnpm nx run ghost-admin:test -- 1 --file-path=tests/acceptance/editor/publish-flow-test.js
  • − pnpm knex-migrator migrate
  • − pnpm reset:data
  • − pnpm reset:data:empty
  • − pnpm docker:build
  • − pnpm docker:clean
  • − pnpm docker:down
  • − pnpm dev:public
  • − pnpm dev:lexical
  • − pnpm dev:analytics
  • − pnpm dev:storage
  • − pnpm dev:stripe
  • − pnpm dev:full
  • − pnpm dev:all
  • − pnpm --filter @tryghost/i18n translate
  • − pnpm --filter @tryghost/i18n lint:translations
  • − pnpm fix
  • − pnpm nx reset
  • − pnpm
  • − pnpm-workspace.yaml
  • − pnpm add
  • − node --conditions=source --import=tsx
  • − tsc
  • − node
  • − pnpm setup
  • − pnpm --filter @tryghost/i18n test
  • − eslint-plugin-ghost: catalog:
  • + pnpm dlx shadcn@latest add <component-name>
  • + pnpm storybook
  • + pnpm build-storybook
  • + pnpm test
  • + pnpm test:types
  •   pnpm build
  •   pnpm test:unit
  •   pnpm lint

Section tags

8 shared · 6 only in A · 2 only in B
  • − setup
  • − testing-strategy
  • − security
  • − dependencies
  • − database
  • − monorepo
  • + do-not
  • + agent-behaviour
  •   build
  •   test
  •   lint-format
  •   code-style
  •   architecture
  •   git-pr
  •   ui
  •   docs

Line diff

+151 added−333 removed63 unchanged15.9% identical
TryGhost/Ghost · AGENTS.md
@@ −1 @@
1# AGENTS.md
2 
3This file provides guidance to AI Agents when working with code in this repository.
4 
5## Package Manager
6 
7**Always use `pnpm` for all commands.** This repository uses pnpm workspaces, not npm.
 
 
 
 
 
 
 
 
 
 
 
 
8 
9Shared dependency versions are pinned in `pnpm-workspace.yaml` under `catalog:` and referenced as `"pkg": "catalog:"` (or `catalog:<name>` for named catalogs). `catalogMode` is `strict`, so `pnpm add` routes new deps into the catalog automatically — don't inline the version.
10 
11## Monorepo Structure
 
 
 
 
 
 
12 
13Ghost is a pnpm + Nx monorepo with four workspace groups:
14 
15### ghost/* - Core Ghost packages
16- **ghost/core** - Main Ghost application (Node.js/Express backend)
17 - Core server: `ghost/core/core/server/`
18 - Frontend rendering: `ghost/core/core/frontend/`
19 
20### apps/* - React-based UI applications
21Two categories of apps:
22 
23**Admin Apps** (embedded in Ghost Admin):
24- `ember-admin` - Ember.js admin client (legacy, being migrated to React)
25- `admin` - The consolidated React admin shell, organized by domain (`src/{analytics,members,posts,tags,comments,automations,...}`)
26- `admin-x-settings`, `activitypub` - Settings and ActivityPub integration (route-composed into `admin`)
27- Built with Vite + React + `@tanstack/react-query`
28 
29**Public Apps** (served to site visitors):
30- `portal`, `comments-ui`, `signup-form`, `sodo-search`, `announcement-bar`
31- Built as UMD bundles, loaded via CDN in site themes
 
 
32 
33**Foundation Libraries**:
34- `admin-x-framework` - Shared API hooks, routing, utilities
35- `admin-x-design-system` - Legacy design system (being phased out)
36- `shade` - New design system (shadcn/ui + Radix UI + react-hook-form + zod)
37 
38### koenig/* - Ghost editor (Koenig) packages
39Merged from the former TryGhost/Koenig repo with full git history:
40 
41- **koenig-lexical** - The Lexical-based rich text editor UI. Bundled into
42 Ghost Admin at build time (`apps/ember-admin` copies its UMD build into admin
43 assets; `apps/admin` imports it directly)
44- **kg-*** - Editor support packages: server-side renderers and converters
45 consumed by `ghost/core` (kg-default-nodes, kg-lexical-html-renderer,
46 kg-html-to-lexical, ...) plus frontend helpers (kg-unsplash-selector)
47 
48All Koenig packages resolve via `workspace:` — nothing in dev, CI, or the
49release archive installs them from npm. They are published to npm for
50external consumers only, automatically as part of the Ghost release lane
51(see `publish_koenig_packages` in ci.yml).
52 
53**Zero-build dev via the `source` export condition.** The `kg-*` libraries
54consumed by `ghost/core` (and `packages/parse-email-address`) declare a `source`
55condition in their `package.json` `exports` that points at the raw
56`src/*.ts`, listed *before* `types`/`import`/`require`:
 
 
57 
58```jsonc
59".": {
60 "source": "./src/index.ts", // dev/test: read raw TS
61 "types": "./build/esm/index.d.ts",
62 "import": "./build/esm/index.js",
63 "require": "./build/cjs/index.js" // prod/published: compiled JS
64}
65```
66 
67`ghost/core`'s dev runner (`nodemon.json`: `node --conditions=source --import=tsx`)
68and its Vitest configs (`resolve.conditions: ['source', 'node']` +
69`--import tsx --conditions=source`) activate this condition, so a source change
70in a `kg-*` package is picked up with **no `tsc` rebuild**. Production and the
71published npm tarball run plain `node`, which ignores `source` and uses
72`build/` — and `src/` is excluded from each package's `files` array, so it is
73never shipped. When adding a new backend-consumed TS workspace package, copy
74this `exports` shape (see `packages/parse-email-address`) so it works build-free
75in dev from day one; keep the `^build` graph for `tsc`/type-checking and prod.
76 
77### packages/* - Shared workspace libraries
78Backend and shared libraries consumed via `workspace:` — not published to npm:
79 
80- **i18n** - Centralized internationalization for all apps
81- **parse-email-address** - Email address parsing (see the `source` export
82 condition above)
83- **adapters/** - Adapter base classes (`adapter-base-*`: scheduling, storage,
84 SSO, redirects, route settings)
85- **custom-field-types**, **testing** - Shared field-type definitions and test
86 helpers
87- **_template** - Scaffold for new packages; excluded from the workspace
88 
89### e2e/ - End-to-end tests
90- Playwright-based E2E tests with Docker container isolation
91- See `e2e/CLAUDE.md` for detailed testing guidance
92 
93## Common Commands
 
 
 
 
94 
95### Development
96```bash
97corepack enable pnpm # Enable corepack to use the correct pnpm version
98pnpm run setup # First-time setup (installs deps + submodules + builds workspace packages)
99pnpm dev # Start development (Docker backend + host frontend dev servers)
100```
101 
102> **Fresh worktree / first run — run `pnpm setup` before anything else.** It installs deps and syncs submodules. `pnpm fix` does a clean reinstall if anything misbehaves after a branch switch.
 
 
 
 
 
 
 
103 
104### Building
105```bash
106pnpm build # Build all packages (Nx handles dependencies)
107pnpm build:clean # Clean build artifacts and rebuild
108```
109 
110### Testing
111```bash
112# Unit tests (from root)
113pnpm test:unit # Run all unit tests in all packages
114pnpm test:watch # Watch mode — unified Vitest watcher (ghost/core + all apps)
115 
116# Ghost core tests (from ghost/core/)
117cd ghost/core
118pnpm test:unit # Unit tests only (Vitest, run once)
119pnpm test:watch # Watch mode — ghost/core unit tests only
120pnpm test:integration # Integration tests
121pnpm test:e2e # Server-side e2e suites (webhooks/server/frontend/api) — not browser
122pnpm test:all # All test types
123 
124# These run on sqlite with no extra services. The Redis/MinIO/S3 adapter suites
125# probe for their service and auto-skip when it's down (run `pnpm dev:storage`
126# etc. to exercise them); they always run in CI, which starts the services.
127 
128# E2E browser tests (from root)
129pnpm test:e2e # Run e2e/ Playwright tests
130 
131# Running a single test
132cd ghost/core
133pnpm test:single test/unit/path/to/test.test.js # routes test/unit/* → unit config, test/* → DB config
134 
135# Watch a single DB-backed file (integration/e2e) — the default test:watch only
136# covers unit tests, so point it at the DB config explicitly:
137pnpm exec vitest -c vitest.config.db.ts test/integration/path/to/test.test.js
138 
139# Ember Admin tests (from the repository root)
140pnpm nx run ghost-admin:test
141 
142# Run one Ember Admin test file. Paths are relative to apps/ember-admin.
143# The explicit `1` supplies the numeric value required by the test script's
144# trailing `--parallel` option before additional Ember Exam arguments.
145pnpm nx run ghost-admin:test -- 1 --file-path=tests/acceptance/editor/publish-flow-test.js
146```
147 
148> **Always run Ember Admin tests through Nx.** Running `ember test` or
149> `ember exam` directly from `apps/ember-admin` skips the dependency build
150> graph and commonly fails in fresh worktrees with missing outputs such as
151> `koenig-lexical.umd.js`, `@tryghost/admin-x-framework/hooks`, or
152> `@tryghost/kg-converters`. For focused runs, use Ember Exam's `--file-path`
153> as shown above rather than appending `--filter` to the package script.
154 
155### Linting
156```bash
157pnpm lint # Lint all packages
158cd ghost/core && pnpm lint # Lint Ghost core (server, shared, frontend, tests)
159cd apps/ember-admin && pnpm lint # Lint Ember admin
160```
161 
162### Database
163```bash
164pnpm knex-migrator migrate # Run database migrations
165pnpm reset:data # Reset database with test data (1000 members, 100 posts) (requires pnpm dev running)
166pnpm reset:data:empty # Reset database with no data (requires pnpm dev running)
167```
168 
169### Docker
170```bash
171pnpm docker:build # Build Docker images
172pnpm docker:clean # Stop containers, remove volumes and local images
173pnpm docker:down # Stop containers
174```
175 
176### How `pnpm dev` works
 
 
 
 
 
 
 
 
177 
178The `pnpm dev` command uses a **hybrid Docker + host development** setup:
179 
180**What runs in Docker:**
181- Ghost Core backend (with hot-reload via mounted source)
182- MySQL, Redis, Mailpit
183- Caddy gateway/reverse proxy
184 
185**What runs on host by default:**
186- Admin, legacy Ember admin, Portal, and foundation library dev watchers
187- Optional public UMD app watchers can be added when needed
188 
189**Setup:**
190```bash
191# Start Ghost backend, Admin, Portal, and Docker services
192pnpm dev
 
193 
194# Add optional public apps (comments-ui, sodo-search, signup-form, admin-toolbar)
195pnpm dev:public
196 
197# Develop the Koenig editor against Ghost Admin (adds a koenig-lexical rebuild
198# watcher + preview server; Admin loads the editor from your local build)
199pnpm dev:lexical
 
 
 
 
 
 
200 
201# With optional services (uses Docker Compose file composition)
202pnpm dev:analytics # Include Tinybird analytics
203pnpm dev:storage # Include MinIO S3-compatible object storage
204pnpm dev:stripe # Include Stripe webhook forwarding
205pnpm dev:full # Include analytics, storage, Stripe, and public app watchers
206 
207# Everything available
208pnpm dev:all #
209```
210 
211**Accessing Services:**
212- Ghost: `http://localhost:2368` (database: `ghost_dev`)
213- Mailpit UI: `http://localhost:8025` (email testing)
214- MySQL: `localhost:3306`
215- Redis: `localhost:6379`
216- Tinybird: `http://localhost:7181` (when analytics enabled)
217- MinIO Console: `http://localhost:9001` (when storage enabled)
218- MinIO S3 API: `http://localhost:9000` (when storage enabled)
219 
220## Architecture Patterns
221 
222### Admin Apps Integration (Micro-Frontend)
223 
224**Build Process:**
2251. Admin-x React apps build to `apps/*/dist` using Vite
2262. `apps/ember-admin/lib/asset-delivery` copies them to `ghost/core/core/built/admin/assets/*`
2273. Ghost admin serves from `/ghost/assets/{app-name}/{app-name}.js`
228 
229**Runtime Loading:**
230- Ember admin uses `AdminXComponent` to dynamically import React apps
231- React components wrapped in Suspense with error boundaries
232- Apps receive config via `additionalProps()` method
233 
234### Public Apps Integration
235 
236- Built as UMD bundles to `apps/*/umd/*.min.js`
237- Loaded via `<script>` tags in theme templates (injected by `{{ghost_head}}`)
238- Configuration passed via data attributes
239 
240### i18n Architecture
241 
242**Centralized Translations:**
243- Single source: `packages/i18n/locales/{locale}/{namespace}.json`
244- Namespaces: `ghost`, `portal`, `signup-form`, `comments`, `search`
245- 60+ supported locales
246- Context descriptions: `packages/i18n/locales/context.json` — every key must have a non-empty description
247 
248**Translation Workflow:**
249```bash
250pnpm --filter @tryghost/i18n translate # Extract keys from source, update all locale files + context.json
251pnpm --filter @tryghost/i18n lint:translations # Validate interpolation variables across locales
252```
 
253 
254`translate` is run as part of `pnpm --filter @tryghost/i18n test`. In CI, it fails if translation keys or `context.json` are out of date (`failOnUpdate: process.env.CI`). Always run `pnpm --filter @tryghost/i18n translate` after adding or changing `t()` calls.
255 
256**Rules for Translation Keys:**
2571. **Never split sentences across multiple `t()` calls.** Translators cannot reorder words across separate keys. Instead, use `@doist/react-interpolate` to embed React elements (links, bold, etc.) within a single translatable string.
2582. **Always provide context descriptions.** When adding a new key, add a description in `context.json` explaining where the string appears and what it does. CI will reject empty descriptions.
2593. **Use interpolation for dynamic values.** Ghost uses `{variable}` syntax: `t('Welcome back, {name}!', {name: firstname})`
2604. **Use `<tag>` syntax for inline elements.** Combined with `@doist/react-interpolate`: `t('Click <a>here</a> to retry')` with `mapping={{ a: <a href="..." /> }}`
261 
262**Correct pattern (using Interpolate):**
263```jsx
264import Interpolate from '@doist/react-interpolate';
265 
266<Interpolate
267 mapping={{ a: <a href={link} /> }}
268 string={t('Could not sign in. <a>Click here to retry</a>')}
269/>
270```
271 
272**Incorrect pattern (split sentences):**
273```jsx
274// BAD: translators cannot reorder "Click here to retry" relative to the first sentence
275{t('Could not sign in.')} <a href={link}>{t('Click here to retry')}</a>
276```
277 
278See `apps/portal/src/components/pages/email-receiving-faq.js` for a canonical example of correct `Interpolate` usage.
279 
280### Build Dependencies (Nx)
281 
282Critical build order (Nx handles automatically):
2831. `shade` + `admin-x-design-system` build
2842. `admin-x-framework` builds (depends on #1)
2853. Admin apps build (depend on #2)
2864. `apps/ember-admin` builds (depends on #3, copies via asset-delivery)
2875. `ghost/core` serves admin build
288 
289## CSS Architecture
 
 
 
 
 
 
290 
291### TailwindCSS v4 Setup
292 
293Ghost Admin uses **TailwindCSS v4** via the `@tailwindcss/vite` plugin. CSS processing is centralized — only `apps/admin/vite.config.ts` loads the `@tailwindcss/vite` plugin. All embedded React apps (activitypub, admin-x-settings, admin-x-design-system) are scanned from this single entry point.
294 
295### Entry Point
296 
297`apps/admin/src/index.css` is the main CSS entry point. It contains:
298- `@source` directives that scan class usage in shade, activitypub, admin-x-settings, admin-x-design-system, and kg-unsplash-selector
299- `@import "@tryghost/shade/styles.css"` which loads the Shade design system styles
300 
301### Shade Styles
302 
303`apps/shade/styles.css` uses **unlayered** Tailwind imports:
304```css
305@import "tailwindcss/theme.css";
306@import "./preflight.css";
307@import "tailwindcss/utilities.css";
308@import "tw-animate-css";
309@import "./tailwind.theme.css";
310```
311 
312**Why unlayered:** Ember's legacy CSS (`.flex`, `.hidden`, etc.) is unlayered. If Tailwind utilities were in a `@layer`, they would lose to Ember's unlayered CSS in the cascade. Keeping both unlayered means source order determines specificity.
313 
314Theme tokens/variants/animations are defined in CSS (`apps/shade/tailwind.theme.css` + runtime vars in `styles.css`), so there is no JS `@config` bridge in the Admin runtime lane. `tw-animate-css` is the v4 replacement for `tailwindcss-animate`.
315 
316### Critical Rule: Embedded Apps Must NOT Import Shade Independently
317 
318Apps consumed via `@source` (activitypub, admin-x-settings) must **NOT** import `@tryghost/shade/styles.css` in their own CSS. Doing so causes duplicate Tailwind utilities and cascade conflicts. All Tailwind CSS is generated once via the admin entry point.
319 
320### Public Apps
321 
322Public-facing apps (`comments-ui`, `signup-form`, `sodo-search`, `portal`, `announcement-bar`) remain on **TailwindCSS v3**. They are built as UMD bundles for CDN distribution and are independent of the admin CSS pipeline.
323 
324### Legacy Apps
325 
326`admin-x-design-system` and `admin-x-settings` are consumed via `@source` in admin's centralized v4 pipeline for production, and both packages build with CSS-first Tailwind v4 setup.
327 
328## Code Guidelines
329 
330### Commit Messages
331When the user asks you to create a commit or draft a commit message, load and follow the `commit` skill from `.agents/skills/commit`.
332 
333### ESLint Config
334Source of truth: two internal config packages — [`@internal/cfg-eslint`](configs/eslint/index.mjs) (shared rule atoms + the `nodeLibConfig` factory for Node libs) and [`@internal/cfg-eslint-react`](configs/eslint-react/index.mjs) (the `reactAppConfig` factory for every `apps/*` workspace). Both factories are synchronous and have full JSDoc with `@example`s; hover the call site in your editor. Consume them by name — declare the package as a `workspace:*` devDependency.
335 
336Minimal example for a new admin React app (`apps/new-feature/eslint.config.js`):
337 
338```js
339import {reactAppConfig} from '@internal/cfg-eslint-react';
340export default reactAppConfig({
341 tailwindCssPath: `${import.meta.dirname}/../admin/src/index.css`,
342 shadeRestricted: true
343});
344```
345 
346Conventions:
347- **Rules are `'error'` or `'off'` — never `'warn'`.** Warnings get ignored and pollute output. Applies to every workspace covered by the factories above + the standalones; `e2e/` has its own setup (see [e2e/CLAUDE.md](e2e/CLAUDE.md)) and currently still uses warn-level Playwright rules — a separate cleanup.
348- **Params prefixed `legacy*`** (`legacyTailwindV3ConfigPath`, `legacyJsTsSplit`) are escape hatches for migrations that haven't shipped yet. Intentional and visible — PRs to remove them are scoped.
349- **Standalone configs** (`ghost/core`, `apps/ember-admin`, `apps/admin-toolbar`) exist because their rule sets genuinely don't fit a factory — read the file directly. They import shared atoms (`correctnessRules`, `nodeLibRules`, `localFilenamesPlugin`, `strictLinterOptions`) from `@internal/cfg-eslint`.
350- **Plugin deps**: a workspace must declare every eslint plugin its config resolves. Two cases:
351 - *Factory consumers* only import a factory, which supplies its plugins as objects from the config package — so they need just the config package (`@internal/cfg-eslint` / `@internal/cfg-eslint-react`) as a `workspace:*` devDependency, not the individual plugins.
352 - *Hand-rolled configs* (the standalones above, plus the inline configs in `koenig/kg-*` and `e2e/`) `import` plugins directly, so each must list those plugins in its own `devDependencies` — most commonly `eslint-plugin-ghost: catalog:`. Don't rely on the root hoisting a plugin for you; there are no eslint plugins left in the root `package.json` (only `eslint` itself and `globals`, which the root config uses).
353 - Exception: Tailwind — a workspace that uses it must list `tailwindcss` as its own (dev)Dependency regardless (the settings-based resolver requires it locally), and the legacy v3 apps pin `eslint-plugin-tailwindcss` via `catalog:tailwind3`.
354 
355### When Working on Admin UI
356- **New features:** Build in React in `apps/admin` (domain folders under `src/`)
357- **Use:** `admin-x-framework` for API hooks (`useBrowse`, `useEdit`, etc.)
358- **Use:** `shade` design system for new components (not admin-x-design-system)
359- **Translations:** Add to `packages/i18n/locales/en/ghost.json`
360 
361### When Working on Public UI
362- **Edit:** `apps/portal`, `apps/comments-ui`, etc.
363- **Translations:** Separate namespaces (`portal.json`, `comments.json`)
364- **Build:** UMD bundles for CDN distribution
365 
366### When Working on Backend
367- **Core logic:** `ghost/core/core/server/`
368- **Database Schema:** `ghost/core/core/server/data/schema/`
369- **API routes:** `ghost/core/core/server/api/`
370- **Services:** `ghost/core/core/server/services/`
371- **Models:** `ghost/core/core/server/models/`
372- **Frontend & theme rendering:** `ghost/core/core/frontend/`
373 
374### Design System Usage
375- **New components:** Use `shade` (shadcn/ui-inspired)
376- **Legacy:** `admin-x-design-system` (being phased out, avoid for new work)
377 
378### Analytics (Tinybird)
379- **Local development:** `pnpm dev:analytics` (starts Tinybird + MySQL)
380- **Config:** Add Tinybird config to `ghost/core/config.development.json`
381- **Scripts:** `ghost/core/core/server/data/tinybird/scripts/`
382- **Datafiles:** `ghost/core/core/server/data/tinybird/`
383 
384## Troubleshooting
385 
386### Build Issues
387```bash
388pnpm fix # Clean cache + node_modules + reinstall
389pnpm build:clean # Clean build artifacts
390pnpm nx reset # Reset Nx cache
391```
392 
393### Test Issues
394- **E2E failures:** Check `e2e/CLAUDE.md` for debugging tips
395- **Docker issues:** `pnpm docker:clean && pnpm docker:build`
396 
TryGhost/Ghost · apps/shade/AGENTS.md
@@ +1 @@
1# Shade — Agent guide
2 
3Canonical, rule-shaped reference for AI-assisted work on Shade and any admin app that consumes it. Storybook docs at `apps/shade/src/docs/` are the human-facing surface (visual, designer-focused). **This file is the source of truth for decisions.**
4 
5## Core assumptions
6 
7- **Shade is the default source for Ghost Admin UI.** Reach for it first. If a usable primitive, component, recipe, or pattern exists, use it.
8- **Shade is admin-only.** Don't generate install instructions, stylesheet imports, or `ShadeApp` setup snippets — every admin app is already wired up.
9- **Imports come from layer-specific subpaths**, never the root barrel:
10 ```ts
11 import {Stack, Inline, Box, Grid, Container, Text} from '@tryghost/shade/primitives';
12 import {Button, Input, Dialog} from '@tryghost/shade/components';
13 import {PageHeader, KpiCard, Filters} from '@tryghost/shade/patterns';
14 import {ListPage} from '@tryghost/shade/page-templates';
15 import {PostShareModal} from '@tryghost/shade/posts-stats';
16 import {cn} from '@tryghost/shade/utils';
17 import {ShadeApp} from '@tryghost/shade/app';
18 ```
19- Inside Shade itself, use the `@/` alias for cross-file imports.
20 
21## The five layers
22 
23| Layer | Path | Use when | Examples |
24|---|---|---|---|
25| **Tokens** | `theme-variables.css`, `tailwind.theme.css` | You need a colour, size, duration, radius | `--background`, `--text-base`, `--radius-md` |
26| **Primitives** | `src/components/primitives/` | You need layout structure | `Stack`, `Inline`, `Box`, `Grid`, `Container`, `Text` |
27| **Components** | `src/components/ui/` | You need a generic, accessible UI control | `Button`, `Input`, `Dialog`, `Tabs`, `Card`, `DropdownMenu` |
28| **Recipes** | `src/components/ui/<name>.ts` | Several components share the same visual rule (chrome, focus, density) | `inputSurface` |
29| **Patterns** | `src/components/patterns/` | The shape is product-specific and recurs across Admin | `PageHeader`, `Filters`, `KpiCard`, `GhAreaChart` |
30 
31Plus two additional barrels:
32 
33- **`page-templates/`** (`src/components/page-templates/`) — top-level page wrappers (`ListPage` today). Composes Patterns + Components + Primitives. Imported via `@tryghost/shade/page-templates`.
34- **`posts-stats/`** (`src/components/posts-stats/`) — transitional layer for components shared between `apps/posts` and `apps/stats` until those merge. Don't generalise it. Imported via `@tryghost/shade/posts-stats`.
 
 
35 
36## Decision flow: where does new code go?
 
37 
38When building a new UI shape, walk this top-to-bottom and stop at the first match.
 
 
 
 
39 
401. **Is it just a colour, size, radius, duration?** → **Token**. Add to `theme-variables.css` (semantic) or `tailwind.theme.css` (`@theme` raw).
412. **Is it layout-only (spacing, alignment, structure)?** → **Primitive**. Use an existing one (`Stack`, `Inline`, `Box`, `Grid`, `Container`, `Text`); only add a new one if the structural shape is genuinely novel.
423. **Is it a generic, accessible UI control with no Ghost-specific knowledge?** → **Component**. Reuse an existing one in `src/components/ui/`. Only add a new component if it doesn't exist and the rules below pass.
434. **Is it the same chrome / focus / density rule shared across ≥ 2 components?** → **Recipe**. A class-string function next to the components in `src/components/ui/`.
445. **Does it know about Ghost (KPIs, members, posts, newsletters, analytics)?** → **Pattern**.
45 
46Quick gut check: **generic name → Component; Ghost-shaped name → Pattern.** `Button` is web-y; `KpiCard` is Ghost-y.
 
 
 
47 
48## When to ADD to Shade vs keep local
 
49 
50The default is to **keep code local first**. Premature design system additions lock in the wrong API and every consumer pays when you change it.
 
 
 
 
 
51 
52Promote to Shade only when **all** are true:
 
 
 
53 
541. **Reused at least twice in different surfaces.** Not "we might reuse this" — actual second use.
552. **It's generic.** A `<MembersTable>` that's just `<Table>` with three pre-set columns is not a Shade thing; it belongs in the app.
563. **The shape has settled.** Slots and composition have been stable across both local copies for at least one iteration cycle.
574. **It has a generic name.** `PageHeader`, `KpiCard`, `PostShareModal`. Not `MembersFilterBar` or `PostAnalyticsHero` — those name a single surface and will date.
585. **The API is slots, not props.** 3–6 named subcomponents (`.Title`, `.Actions`, `.Body`), not a `<ListPage title="..." onAdd={...} columns={...} />` prop bag.
596. **State stays with the consumer.** No `useQuery`, no routing, no app-context reads inside Shade.
60 
61Fail any of these? Keep it local. Build it again somewhere else first, then promote.
 
 
 
 
 
 
 
62 
63## Conventions
 
 
 
 
 
 
 
 
64 
65### File names
 
66 
67- Files: kebab-case (`dropdown-menu.tsx`) — matches ShadCN CLI output.
68- Components: PascalCase exports (`DropdownMenu`).
69- Hooks, functions, variables: camelCase.
 
 
 
 
 
70 
71### Component file structure
 
 
72 
73- One `<name>.tsx` per component (or compound family).
74- Sibling `<name>.stories.tsx` is required.
75- Use `cn()` to merge classes (`@tryghost/shade/utils` for consumers, `@/lib/utils` inside Shade).
76- Use `cva()` for variants. Forward and merge `className` so consumers can extend without wrapping.
77- For multi-region components, expose compound subcomponents (`.Title`, `.Actions`, …) — not a prop bag.
78 
79### Storybook titles
 
 
 
 
 
80 
81| Layer | Title prefix |
82|---|---|
83| Primitive | `Primitives / <Name>` |
84| Component | `Components / <Name>` |
85| Recipe | `Recipes / <Name>` |
86| Pattern | `Patterns / <Name>` |
87| Posts–Stats interim | `Posts–Stats / <Name>` |
88| Token gallery | `Tokens / <Topic>` |
89 
90Use `tags: ['autodocs']`. Add a short `parameters.docs.description.component`. Per-story `parameters.docs.description.story` is a one-liner explaining when to use that variant.
 
 
 
 
91 
92### Tokens & dark mode
 
 
 
 
93 
94- Use **semantic tokens** (`bg-background`, `text-foreground`, `border-border-default`, `var(--surface-elevated)`) — these flip in dark mode automatically.
95- Never hard-code hex or `hsl()` values, even temporarily.
96- Don't write `dark:` Tailwind variants for colour. The tokens do that. (Exceptions: assets like logos/illustrations.)
97- Inside stylesheets, use `var(--token)` directly. Don't wrap in `hsl()` — the variables already contain `hsl(…)`.
98- New tokens go in `apps/shade/theme-variables.css` (semantic + dark-mode overrides) or `apps/shade/tailwind.theme.css` (raw `@theme`).
 
 
99 
100### Required states for components
 
 
101 
102Every interactive component must work in **default, hover, focus-visible, disabled** before anything else. Optional states (active, loading, error, empty) are documented when they apply. Each state should be visible in the story.
 
103 
104For form controls, drive chrome through the `inputSurface` recipe — don't roll your own focus ring.
 
 
105 
106## ShadCN guardrails
 
 
107 
108Most new components start from a ShadCN install:
 
109 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110```bash
111pnpm dlx shadcn@latest add <component-name>
 
 
112```
113 
114- **Never overwrite an existing Shade component** when the CLI prompts. Choose "No".
115- Run on a fresh branch before installing.
116- If the component already exists, generate into a scratch repo and manually port the parts you want.
117- After integrating: swap raw colours for semantic tokens, ensure the four required states work, trim any props that hint at a specific surface, copy useful examples from `https://ui.shadcn.com/docs/components/<name>` into the story.
118- Use the `@` alias for internal imports (e.g. `@/lib/utils`).
 
119 
120## Build, test, dev
 
 
 
 
 
121 
122| Command | Purpose |
123|---|---|
124| `pnpm storybook` | Run Storybook locally (visual verification) |
125| `pnpm build` | Type declarations + Vite library build to `es/` |
126| `pnpm build-storybook` | Static Storybook export |
127| `pnpm test` | Type-check + Vitest with coverage |
128| `pnpm test:unit` | Unit tests only |
129| `pnpm test:types` | TS type-check only |
130| `pnpm lint` | ESLint (src + tests, `tailwindcss/*` rules enabled) |
131 
132Always run `pnpm lint` before committing.
133 
134## Testing expectations
 
 
 
135 
136Formal testing strategy is TBD. Interim rules:
 
 
137 
138- Vitest + Testing Library + jsdom.
139- Location: `test/unit/**/*.test.(ts|tsx|js)`.
140- Use `test/unit/utils/test-utils.tsx`'s `render` helper when a wrapper is needed.
141- For new UI components, prioritise comprehensive Storybook stories; add focused unit tests where they pay off (hooks, utils, logic-heavy parts).
142- No strict coverage threshold yet — just run `pnpm test` locally and keep it green.
143 
144## Anti-patterns (don't do these)
 
145 
146- **Don't import `@tryghost/shade/styles.css` separately from an embedded admin app.** The admin entry point is the single CSS lane; importing twice causes duplicate utilities and cascade conflicts.
147- **Don't import from the root `@tryghost/shade` barrel.** Use layer-specific subpaths.
148- **Don't add `dark:` variants for colour.** Use semantic tokens.
149- **Don't add product-specific props to a generic Component.** Extract a Pattern wrapper.
150- **Don't put `useQuery` or app-context reads inside a Pattern.** Patterns are layout/composition contracts. Bring-your-own state.
151- **Don't rename ShadCN-generated files** purely for casing.
152- **Don't generalise `posts-stats/`.** It's named for one specific historical situation and goes away on its own.
153- **Don't create new top-level CSS files.** Tokens live in `theme-variables.css` and `tailwind.theme.css`.
154- **Don't add migration / setup / install instructions to component docs.** Shade is admin-only and already wired up.
155 
156## Commit & PR conventions
 
 
 
 
157 
158Commit messages are the release notes.
 
 
159 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160```
161Added Avatar component
162 
163ref https://linear.app/ghost/issue/DES-1234/avatar
164 
165Builds on Radix Avatar with a size variant scale and a fallback initials slot.
 
 
 
 
 
 
 
 
 
 
 
 
 
166```
167 
168- **Line 1**: ≤ 80 chars, past tense. Starts with one of: `Fixed`, `Changed`, `Updated`, `Improved`, `Added`, `Removed`, `Reverted`, `Moved`, `Released`, `Bumped`, `Cleaned`.
169- **Line 2**: blank.
170- **Line 3**: magic word (`ref`, `closes`, `fixes`) + space + **full Linear URL**. Not `ref:` (no colon).
171- **Line 4+**: explain the **why**, not the what.
172- Dependency bumps: focus the message on user-visible changes.
173 
174PRs: describe the change, link the Linear issue, include screenshots or GIFs for any UI change, update or add stories.
175 
176## Acceptance checklist (component)
177 
178Before marking a component done:
 
 
 
 
 
179 
180- [ ] Lives in the right layer (re-read **Decision flow** above)
181- [ ] `className` forwarded and merged with `cn()`
182- [ ] Default, hover, focus-visible, disabled all work and are visible in the story
183- [ ] No hex values, no `bg-gray-200`-style raw palette utilities for UI chrome — semantic tokens only
184- [ ] No product-specific props on a generic Component
185- [ ] Story covers variants + states with one-line "when to use" descriptions
186- [ ] `pnpm lint`, `pnpm test`, and Storybook all clean
187 
188## Repo layout
189 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190```
191apps/shade/
192├── theme-variables.css Runtime semantic tokens + dark mode
193├── tailwind.theme.css Tailwind @theme raw catalogue
194├── .storybook/ Storybook config (preview.tsx controls sort order)
195└── src/
196 ├── components/
197 │ ├── primitives/ Layout primitives
198 │ ├── ui/ Generic controls + recipes
199 │ ├── patterns/ Product compositions
200 │ └── posts-stats/ Interim Posts ↔ Stats shared
201 ├── docs/ MDX + token showcase stories
202 │ ├── showcase/ Internal-only token display components
203 │ └── tokens/ Token visual stories
204 ├── hooks/ Generic React hooks
205 ├── lib/ Utilities (cn, formatters, chart helpers)
206 └── providers/ Context providers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207```
208 
209Entrypoint barrels (`components.ts`, `primitives.ts`, `patterns.ts`) re-export from the matching folder.
 
 
 
 
 
 
 
210 
211## Human docs
 
 
 
 
212 
213The MDX in `src/docs/` and the per-component stories are the **human-facing** surface. They're short, visual, example-driven. If a human-facing rule conflicts with this file, **this file wins** — and that's a sign the MDX needs updating.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214 
@@ −1 +1 @@
1−# AGENTS.md
1+# Shade — Agent guide
22  
3−This file provides guidance to AI Agents when working with code in this repository.
3+Canonical, rule-shaped reference for AI-assisted work on Shade and any admin app that consumes it. Storybook docs at `apps/shade/src/docs/` are the human-facing surface (visual, designer-focused). **This file is the source of truth for decisions.**
44  
5−## Package Manager
5+## Core assumptions
66  
7−**Always use `pnpm` for all commands.** This repository uses pnpm workspaces, not npm.
7+- **Shade is the default source for Ghost Admin UI.** Reach for it first. If a usable primitive, component, recipe, or pattern exists, use it.
8+- **Shade is admin-only.** Don't generate install instructions, stylesheet imports, or `ShadeApp` setup snippets — every admin app is already wired up.
9+- **Imports come from layer-specific subpaths**, never the root barrel:
10+ ```ts
11+ import {Stack, Inline, Box, Grid, Container, Text} from '@tryghost/shade/primitives';
12+ import {Button, Input, Dialog} from '@tryghost/shade/components';
13+ import {PageHeader, KpiCard, Filters} from '@tryghost/shade/patterns';
14+ import {ListPage} from '@tryghost/shade/page-templates';
15+ import {PostShareModal} from '@tryghost/shade/posts-stats';
16+ import {cn} from '@tryghost/shade/utils';
17+ import {ShadeApp} from '@tryghost/shade/app';
18+ ```
19+- Inside Shade itself, use the `@/` alias for cross-file imports.
820  
9−Shared dependency versions are pinned in `pnpm-workspace.yaml` under `catalog:` and referenced as `"pkg": "catalog:"` (or `catalog:<name>` for named catalogs). `catalogMode` is `strict`, so `pnpm add` routes new deps into the catalog automatically — don't inline the version.
21+## The five layers
1022  
11−## Monorepo Structure
23+| Layer | Path | Use when | Examples |
24+|---|---|---|---|
25+| **Tokens** | `theme-variables.css`, `tailwind.theme.css` | You need a colour, size, duration, radius | `--background`, `--text-base`, `--radius-md` |
26+| **Primitives** | `src/components/primitives/` | You need layout structure | `Stack`, `Inline`, `Box`, `Grid`, `Container`, `Text` |
27+| **Components** | `src/components/ui/` | You need a generic, accessible UI control | `Button`, `Input`, `Dialog`, `Tabs`, `Card`, `DropdownMenu` |
28+| **Recipes** | `src/components/ui/<name>.ts` | Several components share the same visual rule (chrome, focus, density) | `inputSurface` |
29+| **Patterns** | `src/components/patterns/` | The shape is product-specific and recurs across Admin | `PageHeader`, `Filters`, `KpiCard`, `GhAreaChart` |
1230  
13−Ghost is a pnpm + Nx monorepo with four workspace groups:
31+Plus two additional barrels:
1432  
15−### ghost/* - Core Ghost packages
16−- **ghost/core** - Main Ghost application (Node.js/Express backend)
17− - Core server: `ghost/core/core/server/`
18− - Frontend rendering: `ghost/core/core/frontend/`
33+- **`page-templates/`** (`src/components/page-templates/`) — top-level page wrappers (`ListPage` today). Composes Patterns + Components + Primitives. Imported via `@tryghost/shade/page-templates`.
34+- **`posts-stats/`** (`src/components/posts-stats/`) — transitional layer for components shared between `apps/posts` and `apps/stats` until those merge. Don't generalise it. Imported via `@tryghost/shade/posts-stats`.
1935  
20−### apps/* - React-based UI applications
21−Two categories of apps:
36+## Decision flow: where does new code go?
2237  
23−**Admin Apps** (embedded in Ghost Admin):
24−- `ember-admin` - Ember.js admin client (legacy, being migrated to React)
25−- `admin` - The consolidated React admin shell, organized by domain (`src/{analytics,members,posts,tags,comments,automations,...}`)
26−- `admin-x-settings`, `activitypub` - Settings and ActivityPub integration (route-composed into `admin`)
27−- Built with Vite + React + `@tanstack/react-query`
38+When building a new UI shape, walk this top-to-bottom and stop at the first match.
2839  
29−**Public Apps** (served to site visitors):
30−- `portal`, `comments-ui`, `signup-form`, `sodo-search`, `announcement-bar`
31−- Built as UMD bundles, loaded via CDN in site themes
40+1. **Is it just a colour, size, radius, duration?** → **Token**. Add to `theme-variables.css` (semantic) or `tailwind.theme.css` (`@theme` raw).
41+2. **Is it layout-only (spacing, alignment, structure)?** → **Primitive**. Use an existing one (`Stack`, `Inline`, `Box`, `Grid`, `Container`, `Text`); only add a new one if the structural shape is genuinely novel.
42+3. **Is it a generic, accessible UI control with no Ghost-specific knowledge?** → **Component**. Reuse an existing one in `src/components/ui/`. Only add a new component if it doesn't exist and the rules below pass.
43+4. **Is it the same chrome / focus / density rule shared across ≥ 2 components?** → **Recipe**. A class-string function next to the components in `src/components/ui/`.
44+5. **Does it know about Ghost (KPIs, members, posts, newsletters, analytics)?** → **Pattern**.
3245  
33−**Foundation Libraries**:
34−- `admin-x-framework` - Shared API hooks, routing, utilities
35−- `admin-x-design-system` - Legacy design system (being phased out)
36−- `shade` - New design system (shadcn/ui + Radix UI + react-hook-form + zod)
46+Quick gut check: **generic name → Component; Ghost-shaped name → Pattern.** `Button` is web-y; `KpiCard` is Ghost-y.
3747  
38−### koenig/* - Ghost editor (Koenig) packages
39−Merged from the former TryGhost/Koenig repo with full git history:
48+## When to ADD to Shade vs keep local
4049  
41−- **koenig-lexical** - The Lexical-based rich text editor UI. Bundled into
42− Ghost Admin at build time (`apps/ember-admin` copies its UMD build into admin
43− assets; `apps/admin` imports it directly)
44−- **kg-*** - Editor support packages: server-side renderers and converters
45− consumed by `ghost/core` (kg-default-nodes, kg-lexical-html-renderer,
46− kg-html-to-lexical, ...) plus frontend helpers (kg-unsplash-selector)
50+The default is to **keep code local first**. Premature design system additions lock in the wrong API and every consumer pays when you change it.
4751  
48−All Koenig packages resolve via `workspace:` — nothing in dev, CI, or the
49−release archive installs them from npm. They are published to npm for
50−external consumers only, automatically as part of the Ghost release lane
51−(see `publish_koenig_packages` in ci.yml).
52+Promote to Shade only when **all** are true:
5253  
53−**Zero-build dev via the `source` export condition.** The `kg-*` libraries
54−consumed by `ghost/core` (and `packages/parse-email-address`) declare a `source`
55−condition in their `package.json` `exports` that points at the raw
56−`src/*.ts`, listed *before* `types`/`import`/`require`:
54+1. **Reused at least twice in different surfaces.** Not "we might reuse this" — actual second use.
55+2. **It's generic.** A `<MembersTable>` that's just `<Table>` with three pre-set columns is not a Shade thing; it belongs in the app.
56+3. **The shape has settled.** Slots and composition have been stable across both local copies for at least one iteration cycle.
57+4. **It has a generic name.** `PageHeader`, `KpiCard`, `PostShareModal`. Not `MembersFilterBar` or `PostAnalyticsHero` — those name a single surface and will date.
58+5. **The API is slots, not props.** 3–6 named subcomponents (`.Title`, `.Actions`, `.Body`), not a `<ListPage title="..." onAdd={...} columns={...} />` prop bag.
59+6. **State stays with the consumer.** No `useQuery`, no routing, no app-context reads inside Shade.
5760  
58−```jsonc
59−".": {
60− "source": "./src/index.ts", // dev/test: read raw TS
61− "types": "./build/esm/index.d.ts",
62− "import": "./build/esm/index.js",
63− "require": "./build/cjs/index.js" // prod/published: compiled JS
64−}
65−```
61+Fail any of these? Keep it local. Build it again somewhere else first, then promote.
6662  
67−`ghost/core`'s dev runner (`nodemon.json`: `node --conditions=source --import=tsx`)
68−and its Vitest configs (`resolve.conditions: ['source', 'node']` +
69−`--import tsx --conditions=source`) activate this condition, so a source change
70−in a `kg-*` package is picked up with **no `tsc` rebuild**. Production and the
71−published npm tarball run plain `node`, which ignores `source` and uses
72−`build/` — and `src/` is excluded from each package's `files` array, so it is
73−never shipped. When adding a new backend-consumed TS workspace package, copy
74−this `exports` shape (see `packages/parse-email-address`) so it works build-free
75−in dev from day one; keep the `^build` graph for `tsc`/type-checking and prod.
63+## Conventions
7664  
77−### packages/* - Shared workspace libraries
78−Backend and shared libraries consumed via `workspace:` — not published to npm:
65+### File names
7966  
80−- **i18n** - Centralized internationalization for all apps
81−- **parse-email-address** - Email address parsing (see the `source` export
82− condition above)
83−- **adapters/** - Adapter base classes (`adapter-base-*`: scheduling, storage,
84− SSO, redirects, route settings)
85−- **custom-field-types**, **testing** - Shared field-type definitions and test
86− helpers
87−- **_template** - Scaffold for new packages; excluded from the workspace
67+- Files: kebab-case (`dropdown-menu.tsx`) — matches ShadCN CLI output.
68+- Components: PascalCase exports (`DropdownMenu`).
69+- Hooks, functions, variables: camelCase.
8870  
89−### e2e/ - End-to-end tests
90−- Playwright-based E2E tests with Docker container isolation
91−- See `e2e/CLAUDE.md` for detailed testing guidance
71+### Component file structure
9272  
93−## Common Commands
73+- One `<name>.tsx` per component (or compound family).
74+- Sibling `<name>.stories.tsx` is required.
75+- Use `cn()` to merge classes (`@tryghost/shade/utils` for consumers, `@/lib/utils` inside Shade).
76+- Use `cva()` for variants. Forward and merge `className` so consumers can extend without wrapping.
77+- For multi-region components, expose compound subcomponents (`.Title`, `.Actions`, …) — not a prop bag.
9478  
95−### Development
96−```bash
97−corepack enable pnpm # Enable corepack to use the correct pnpm version
98−pnpm run setup # First-time setup (installs deps + submodules + builds workspace packages)
99−pnpm dev # Start development (Docker backend + host frontend dev servers)
100−```
79+### Storybook titles
10180  
102−> **Fresh worktree / first run — run `pnpm setup` before anything else.** It installs deps and syncs submodules. `pnpm fix` does a clean reinstall if anything misbehaves after a branch switch.
81+| Layer | Title prefix |
82+|---|---|
83+| Primitive | `Primitives / <Name>` |
84+| Component | `Components / <Name>` |
85+| Recipe | `Recipes / <Name>` |
86+| Pattern | `Patterns / <Name>` |
87+| Posts–Stats interim | `Posts–Stats / <Name>` |
88+| Token gallery | `Tokens / <Topic>` |
10389  
104−### Building
105−```bash
106−pnpm build # Build all packages (Nx handles dependencies)
107−pnpm build:clean # Clean build artifacts and rebuild
108−```
90+Use `tags: ['autodocs']`. Add a short `parameters.docs.description.component`. Per-story `parameters.docs.description.story` is a one-liner explaining when to use that variant.
10991  
110−### Testing
111−```bash
112−# Unit tests (from root)
113−pnpm test:unit # Run all unit tests in all packages
114−pnpm test:watch # Watch mode — unified Vitest watcher (ghost/core + all apps)
92+### Tokens & dark mode
11593  
116−# Ghost core tests (from ghost/core/)
117−cd ghost/core
118−pnpm test:unit # Unit tests only (Vitest, run once)
119−pnpm test:watch # Watch mode — ghost/core unit tests only
120−pnpm test:integration # Integration tests
121−pnpm test:e2e # Server-side e2e suites (webhooks/server/frontend/api) — not browser
122−pnpm test:all # All test types
94+- Use **semantic tokens** (`bg-background`, `text-foreground`, `border-border-default`, `var(--surface-elevated)`) — these flip in dark mode automatically.
95+- Never hard-code hex or `hsl()` values, even temporarily.
96+- Don't write `dark:` Tailwind variants for colour. The tokens do that. (Exceptions: assets like logos/illustrations.)
97+- Inside stylesheets, use `var(--token)` directly. Don't wrap in `hsl()` — the variables already contain `hsl(…)`.
98+- New tokens go in `apps/shade/theme-variables.css` (semantic + dark-mode overrides) or `apps/shade/tailwind.theme.css` (raw `@theme`).
12399  
124−# These run on sqlite with no extra services. The Redis/MinIO/S3 adapter suites
125−# probe for their service and auto-skip when it's down (run `pnpm dev:storage`
126−# etc. to exercise them); they always run in CI, which starts the services.
100+### Required states for components
127101  
128−# E2E browser tests (from root)
129−pnpm test:e2e # Run e2e/ Playwright tests
102+Every interactive component must work in **default, hover, focus-visible, disabled** before anything else. Optional states (active, loading, error, empty) are documented when they apply. Each state should be visible in the story.
130103  
131−# Running a single test
132−cd ghost/core
133−pnpm test:single test/unit/path/to/test.test.js # routes test/unit/* → unit config, test/* → DB config
104+For form controls, drive chrome through the `inputSurface` recipe — don't roll your own focus ring.
134105  
135−# Watch a single DB-backed file (integration/e2e) — the default test:watch only
136−# covers unit tests, so point it at the DB config explicitly:
137−pnpm exec vitest -c vitest.config.db.ts test/integration/path/to/test.test.js
106+## ShadCN guardrails
138107  
139−# Ember Admin tests (from the repository root)
140−pnpm nx run ghost-admin:test
108+Most new components start from a ShadCN install:
141109  
142−# Run one Ember Admin test file. Paths are relative to apps/ember-admin.
143−# The explicit `1` supplies the numeric value required by the test script's
144−# trailing `--parallel` option before additional Ember Exam arguments.
145−pnpm nx run ghost-admin:test -- 1 --file-path=tests/acceptance/editor/publish-flow-test.js
146−```
147− 
148−> **Always run Ember Admin tests through Nx.** Running `ember test` or
149−> `ember exam` directly from `apps/ember-admin` skips the dependency build
150−> graph and commonly fails in fresh worktrees with missing outputs such as
151−> `koenig-lexical.umd.js`, `@tryghost/admin-x-framework/hooks`, or
152−> `@tryghost/kg-converters`. For focused runs, use Ember Exam's `--file-path`
153−> as shown above rather than appending `--filter` to the package script.
154− 
155−### Linting
156110 ```bash
157−pnpm lint # Lint all packages
158−cd ghost/core && pnpm lint # Lint Ghost core (server, shared, frontend, tests)
159−cd apps/ember-admin && pnpm lint # Lint Ember admin
111+pnpm dlx shadcn@latest add <component-name>
160112 ```
161113  
162−### Database
163−```bash
164−pnpm knex-migrator migrate # Run database migrations
165−pnpm reset:data # Reset database with test data (1000 members, 100 posts) (requires pnpm dev running)
166−pnpm reset:data:empty # Reset database with no data (requires pnpm dev running)
167−```
114+- **Never overwrite an existing Shade component** when the CLI prompts. Choose "No".
115+- Run on a fresh branch before installing.
116+- If the component already exists, generate into a scratch repo and manually port the parts you want.
117+- After integrating: swap raw colours for semantic tokens, ensure the four required states work, trim any props that hint at a specific surface, copy useful examples from `https://ui.shadcn.com/docs/components/<name>` into the story.
118+- Use the `@` alias for internal imports (e.g. `@/lib/utils`).
168119  
169−### Docker
170−```bash
171−pnpm docker:build # Build Docker images
172−pnpm docker:clean # Stop containers, remove volumes and local images
173−pnpm docker:down # Stop containers
174−```
120+## Build, test, dev
175121  
176−### How `pnpm dev` works
122+| Command | Purpose |
123+|---|---|
124+| `pnpm storybook` | Run Storybook locally (visual verification) |
125+| `pnpm build` | Type declarations + Vite library build to `es/` |
126+| `pnpm build-storybook` | Static Storybook export |
127+| `pnpm test` | Type-check + Vitest with coverage |
128+| `pnpm test:unit` | Unit tests only |
129+| `pnpm test:types` | TS type-check only |
130+| `pnpm lint` | ESLint (src + tests, `tailwindcss/*` rules enabled) |
177131  
178−The `pnpm dev` command uses a **hybrid Docker + host development** setup:
132+Always run `pnpm lint` before committing.
179133  
180−**What runs in Docker:**
181−- Ghost Core backend (with hot-reload via mounted source)
182−- MySQL, Redis, Mailpit
183−- Caddy gateway/reverse proxy
134+## Testing expectations
184135  
185−**What runs on host by default:**
186−- Admin, legacy Ember admin, Portal, and foundation library dev watchers
187−- Optional public UMD app watchers can be added when needed
136+Formal testing strategy is TBD. Interim rules:
188137  
189−**Setup:**
190−```bash
191−# Start Ghost backend, Admin, Portal, and Docker services
192−pnpm dev
138+- Vitest + Testing Library + jsdom.
139+- Location: `test/unit/**/*.test.(ts|tsx|js)`.
140+- Use `test/unit/utils/test-utils.tsx`'s `render` helper when a wrapper is needed.
141+- For new UI components, prioritise comprehensive Storybook stories; add focused unit tests where they pay off (hooks, utils, logic-heavy parts).
142+- No strict coverage threshold yet — just run `pnpm test` locally and keep it green.
193143  
194−# Add optional public apps (comments-ui, sodo-search, signup-form, admin-toolbar)
195−pnpm dev:public
144+## Anti-patterns (don't do these)
196145  
197−# Develop the Koenig editor against Ghost Admin (adds a koenig-lexical rebuild
198−# watcher + preview server; Admin loads the editor from your local build)
199−pnpm dev:lexical
146+- **Don't import `@tryghost/shade/styles.css` separately from an embedded admin app.** The admin entry point is the single CSS lane; importing twice causes duplicate utilities and cascade conflicts.
147+- **Don't import from the root `@tryghost/shade` barrel.** Use layer-specific subpaths.
148+- **Don't add `dark:` variants for colour.** Use semantic tokens.
149+- **Don't add product-specific props to a generic Component.** Extract a Pattern wrapper.
150+- **Don't put `useQuery` or app-context reads inside a Pattern.** Patterns are layout/composition contracts. Bring-your-own state.
151+- **Don't rename ShadCN-generated files** purely for casing.
152+- **Don't generalise `posts-stats/`.** It's named for one specific historical situation and goes away on its own.
153+- **Don't create new top-level CSS files.** Tokens live in `theme-variables.css` and `tailwind.theme.css`.
154+- **Don't add migration / setup / install instructions to component docs.** Shade is admin-only and already wired up.
200155  
201−# With optional services (uses Docker Compose file composition)
202−pnpm dev:analytics # Include Tinybird analytics
203−pnpm dev:storage # Include MinIO S3-compatible object storage
204−pnpm dev:stripe # Include Stripe webhook forwarding
205−pnpm dev:full # Include analytics, storage, Stripe, and public app watchers
156+## Commit & PR conventions
206157  
207−# Everything available
208−pnpm dev:all #
209−```
158+Commit messages are the release notes.
210159  
211−**Accessing Services:**
212−- Ghost: `http://localhost:2368` (database: `ghost_dev`)
213−- Mailpit UI: `http://localhost:8025` (email testing)
214−- MySQL: `localhost:3306`
215−- Redis: `localhost:6379`
216−- Tinybird: `http://localhost:7181` (when analytics enabled)
217−- MinIO Console: `http://localhost:9001` (when storage enabled)
218−- MinIO S3 API: `http://localhost:9000` (when storage enabled)
219− 
220−## Architecture Patterns
221− 
222−### Admin Apps Integration (Micro-Frontend)
223− 
224−**Build Process:**
225−1. Admin-x React apps build to `apps/*/dist` using Vite
226−2. `apps/ember-admin/lib/asset-delivery` copies them to `ghost/core/core/built/admin/assets/*`
227−3. Ghost admin serves from `/ghost/assets/{app-name}/{app-name}.js`
228− 
229−**Runtime Loading:**
230−- Ember admin uses `AdminXComponent` to dynamically import React apps
231−- React components wrapped in Suspense with error boundaries
232−- Apps receive config via `additionalProps()` method
233− 
234−### Public Apps Integration
235− 
236−- Built as UMD bundles to `apps/*/umd/*.min.js`
237−- Loaded via `<script>` tags in theme templates (injected by `{{ghost_head}}`)
238−- Configuration passed via data attributes
239− 
240−### i18n Architecture
241− 
242−**Centralized Translations:**
243−- Single source: `packages/i18n/locales/{locale}/{namespace}.json`
244−- Namespaces: `ghost`, `portal`, `signup-form`, `comments`, `search`
245−- 60+ supported locales
246−- Context descriptions: `packages/i18n/locales/context.json` — every key must have a non-empty description
247− 
248−**Translation Workflow:**
249−```bash
250−pnpm --filter @tryghost/i18n translate # Extract keys from source, update all locale files + context.json
251−pnpm --filter @tryghost/i18n lint:translations # Validate interpolation variables across locales
252160 ```
161+Added Avatar component
253162  
254−`translate` is run as part of `pnpm --filter @tryghost/i18n test`. In CI, it fails if translation keys or `context.json` are out of date (`failOnUpdate: process.env.CI`). Always run `pnpm --filter @tryghost/i18n translate` after adding or changing `t()` calls.
163+ref https://linear.app/ghost/issue/DES-1234/avatar
255164  
256−**Rules for Translation Keys:**
257−1. **Never split sentences across multiple `t()` calls.** Translators cannot reorder words across separate keys. Instead, use `@doist/react-interpolate` to embed React elements (links, bold, etc.) within a single translatable string.
258−2. **Always provide context descriptions.** When adding a new key, add a description in `context.json` explaining where the string appears and what it does. CI will reject empty descriptions.
259−3. **Use interpolation for dynamic values.** Ghost uses `{variable}` syntax: `t('Welcome back, {name}!', {name: firstname})`
260−4. **Use `<tag>` syntax for inline elements.** Combined with `@doist/react-interpolate`: `t('Click <a>here</a> to retry')` with `mapping={{ a: <a href="..." /> }}`
261− 
262−**Correct pattern (using Interpolate):**
263−```jsx
264−import Interpolate from '@doist/react-interpolate';
265− 
266−<Interpolate
267− mapping={{ a: <a href={link} /> }}
268− string={t('Could not sign in. <a>Click here to retry</a>')}
269−/>
165+Builds on Radix Avatar with a size variant scale and a fallback initials slot.
270166 ```
271167  
272−**Incorrect pattern (split sentences):**
273−```jsx
274−// BAD: translators cannot reorder "Click here to retry" relative to the first sentence
275−{t('Could not sign in.')} <a href={link}>{t('Click here to retry')}</a>
276−```
168+- **Line 1**: ≤ 80 chars, past tense. Starts with one of: `Fixed`, `Changed`, `Updated`, `Improved`, `Added`, `Removed`, `Reverted`, `Moved`, `Released`, `Bumped`, `Cleaned`.
169+- **Line 2**: blank.
170+- **Line 3**: magic word (`ref`, `closes`, `fixes`) + space + **full Linear URL**. Not `ref:` (no colon).
171+- **Line 4+**: explain the **why**, not the what.
172+- Dependency bumps: focus the message on user-visible changes.
277173  
278−See `apps/portal/src/components/pages/email-receiving-faq.js` for a canonical example of correct `Interpolate` usage.
174+PRs: describe the change, link the Linear issue, include screenshots or GIFs for any UI change, update or add stories.
279175  
280−### Build Dependencies (Nx)
176+## Acceptance checklist (component)
281177  
282−Critical build order (Nx handles automatically):
283−1. `shade` + `admin-x-design-system` build
284−2. `admin-x-framework` builds (depends on #1)
285−3. Admin apps build (depend on #2)
286−4. `apps/ember-admin` builds (depends on #3, copies via asset-delivery)
287−5. `ghost/core` serves admin build
178+Before marking a component done:
288179  
289−## CSS Architecture
180+- [ ] Lives in the right layer (re-read **Decision flow** above)
181+- [ ] `className` forwarded and merged with `cn()`
182+- [ ] Default, hover, focus-visible, disabled all work and are visible in the story
183+- [ ] No hex values, no `bg-gray-200`-style raw palette utilities for UI chrome — semantic tokens only
184+- [ ] No product-specific props on a generic Component
185+- [ ] Story covers variants + states with one-line "when to use" descriptions
186+- [ ] `pnpm lint`, `pnpm test`, and Storybook all clean
290187  
291−### TailwindCSS v4 Setup
188+## Repo layout
292189  
293−Ghost Admin uses **TailwindCSS v4** via the `@tailwindcss/vite` plugin. CSS processing is centralized — only `apps/admin/vite.config.ts` loads the `@tailwindcss/vite` plugin. All embedded React apps (activitypub, admin-x-settings, admin-x-design-system) are scanned from this single entry point.
294− 
295−### Entry Point
296− 
297−`apps/admin/src/index.css` is the main CSS entry point. It contains:
298−- `@source` directives that scan class usage in shade, activitypub, admin-x-settings, admin-x-design-system, and kg-unsplash-selector
299−- `@import "@tryghost/shade/styles.css"` which loads the Shade design system styles
300− 
301−### Shade Styles
302− 
303−`apps/shade/styles.css` uses **unlayered** Tailwind imports:
304−```css
305−@import "tailwindcss/theme.css";
306−@import "./preflight.css";
307−@import "tailwindcss/utilities.css";
308−@import "tw-animate-css";
309−@import "./tailwind.theme.css";
310190 ```
311− 
312−**Why unlayered:** Ember's legacy CSS (`.flex`, `.hidden`, etc.) is unlayered. If Tailwind utilities were in a `@layer`, they would lose to Ember's unlayered CSS in the cascade. Keeping both unlayered means source order determines specificity.
313− 
314−Theme tokens/variants/animations are defined in CSS (`apps/shade/tailwind.theme.css` + runtime vars in `styles.css`), so there is no JS `@config` bridge in the Admin runtime lane. `tw-animate-css` is the v4 replacement for `tailwindcss-animate`.
315− 
316−### Critical Rule: Embedded Apps Must NOT Import Shade Independently
317− 
318−Apps consumed via `@source` (activitypub, admin-x-settings) must **NOT** import `@tryghost/shade/styles.css` in their own CSS. Doing so causes duplicate Tailwind utilities and cascade conflicts. All Tailwind CSS is generated once via the admin entry point.
319− 
320−### Public Apps
321− 
322−Public-facing apps (`comments-ui`, `signup-form`, `sodo-search`, `portal`, `announcement-bar`) remain on **TailwindCSS v3**. They are built as UMD bundles for CDN distribution and are independent of the admin CSS pipeline.
323− 
324−### Legacy Apps
325− 
326−`admin-x-design-system` and `admin-x-settings` are consumed via `@source` in admin's centralized v4 pipeline for production, and both packages build with CSS-first Tailwind v4 setup.
327− 
328−## Code Guidelines
329− 
330−### Commit Messages
331−When the user asks you to create a commit or draft a commit message, load and follow the `commit` skill from `.agents/skills/commit`.
332− 
333−### ESLint Config
334−Source of truth: two internal config packages — [`@internal/cfg-eslint`](configs/eslint/index.mjs) (shared rule atoms + the `nodeLibConfig` factory for Node libs) and [`@internal/cfg-eslint-react`](configs/eslint-react/index.mjs) (the `reactAppConfig` factory for every `apps/*` workspace). Both factories are synchronous and have full JSDoc with `@example`s; hover the call site in your editor. Consume them by name — declare the package as a `workspace:*` devDependency.
335− 
336−Minimal example for a new admin React app (`apps/new-feature/eslint.config.js`):
337− 
338−```js
339−import {reactAppConfig} from '@internal/cfg-eslint-react';
340−export default reactAppConfig({
341− tailwindCssPath: `${import.meta.dirname}/../admin/src/index.css`,
342− shadeRestricted: true
343−});
191+apps/shade/
192+├── theme-variables.css Runtime semantic tokens + dark mode
193+├── tailwind.theme.css Tailwind @theme raw catalogue
194+├── .storybook/ Storybook config (preview.tsx controls sort order)
195+└── src/
196+ ├── components/
197+ │ ├── primitives/ Layout primitives
198+ │ ├── ui/ Generic controls + recipes
199+ │ ├── patterns/ Product compositions
200+ │ └── posts-stats/ Interim Posts ↔ Stats shared
201+ ├── docs/ MDX + token showcase stories
202+ │ ├── showcase/ Internal-only token display components
203+ │ └── tokens/ Token visual stories
204+ ├── hooks/ Generic React hooks
205+ ├── lib/ Utilities (cn, formatters, chart helpers)
206+ └── providers/ Context providers
344207 ```
345208  
346−Conventions:
347−- **Rules are `'error'` or `'off'` — never `'warn'`.** Warnings get ignored and pollute output. Applies to every workspace covered by the factories above + the standalones; `e2e/` has its own setup (see [e2e/CLAUDE.md](e2e/CLAUDE.md)) and currently still uses warn-level Playwright rules — a separate cleanup.
348−- **Params prefixed `legacy*`** (`legacyTailwindV3ConfigPath`, `legacyJsTsSplit`) are escape hatches for migrations that haven't shipped yet. Intentional and visible — PRs to remove them are scoped.
349−- **Standalone configs** (`ghost/core`, `apps/ember-admin`, `apps/admin-toolbar`) exist because their rule sets genuinely don't fit a factory — read the file directly. They import shared atoms (`correctnessRules`, `nodeLibRules`, `localFilenamesPlugin`, `strictLinterOptions`) from `@internal/cfg-eslint`.
350−- **Plugin deps**: a workspace must declare every eslint plugin its config resolves. Two cases:
351− - *Factory consumers* only import a factory, which supplies its plugins as objects from the config package — so they need just the config package (`@internal/cfg-eslint` / `@internal/cfg-eslint-react`) as a `workspace:*` devDependency, not the individual plugins.
352− - *Hand-rolled configs* (the standalones above, plus the inline configs in `koenig/kg-*` and `e2e/`) `import` plugins directly, so each must list those plugins in its own `devDependencies` — most commonly `eslint-plugin-ghost: catalog:`. Don't rely on the root hoisting a plugin for you; there are no eslint plugins left in the root `package.json` (only `eslint` itself and `globals`, which the root config uses).
353− - Exception: Tailwind — a workspace that uses it must list `tailwindcss` as its own (dev)Dependency regardless (the settings-based resolver requires it locally), and the legacy v3 apps pin `eslint-plugin-tailwindcss` via `catalog:tailwind3`.
209+Entrypoint barrels (`components.ts`, `primitives.ts`, `patterns.ts`) re-export from the matching folder.
354210  
355−### When Working on Admin UI
356−- **New features:** Build in React in `apps/admin` (domain folders under `src/`)
357−- **Use:** `admin-x-framework` for API hooks (`useBrowse`, `useEdit`, etc.)
358−- **Use:** `shade` design system for new components (not admin-x-design-system)
359−- **Translations:** Add to `packages/i18n/locales/en/ghost.json`
211+## Human docs
360212  
361−### When Working on Public UI
362−- **Edit:** `apps/portal`, `apps/comments-ui`, etc.
363−- **Translations:** Separate namespaces (`portal.json`, `comments.json`)
364−- **Build:** UMD bundles for CDN distribution
365− 
366−### When Working on Backend
367−- **Core logic:** `ghost/core/core/server/`
368−- **Database Schema:** `ghost/core/core/server/data/schema/`
369−- **API routes:** `ghost/core/core/server/api/`
370−- **Services:** `ghost/core/core/server/services/`
371−- **Models:** `ghost/core/core/server/models/`
372−- **Frontend & theme rendering:** `ghost/core/core/frontend/`
373− 
374−### Design System Usage
375−- **New components:** Use `shade` (shadcn/ui-inspired)
376−- **Legacy:** `admin-x-design-system` (being phased out, avoid for new work)
377− 
378−### Analytics (Tinybird)
379−- **Local development:** `pnpm dev:analytics` (starts Tinybird + MySQL)
380−- **Config:** Add Tinybird config to `ghost/core/config.development.json`
381−- **Scripts:** `ghost/core/core/server/data/tinybird/scripts/`
382−- **Datafiles:** `ghost/core/core/server/data/tinybird/`
383− 
384−## Troubleshooting
385− 
386−### Build Issues
387−```bash
388−pnpm fix # Clean cache + node_modules + reinstall
389−pnpm build:clean # Clean build artifacts
390−pnpm nx reset # Reset Nx cache
391−```
392− 
393−### Test Issues
394−- **E2E failures:** Check `e2e/CLAUDE.md` for debugging tips
395−- **Docker issues:** `pnpm docker:clean && pnpm docker:build`
213+The MDX in `src/docs/` and the per-component stories are the **human-facing** surface. They're short, visual, example-driven. If a human-facing rule conflicts with this file, **this file wins** — and that's a sign the MDX needs updating.
396214  
RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack