| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 58 | 10 | 0% |
| Commands | 1 | 39 | 7 | 2% |
| Section tags | 3 | 11 | 1 | 20% |
What each file covers
Sections
0 shared · 58 only in A · 10 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
- + Koenig Lexical Test Guide
- + Test Commands
- + Unit Tests
- + Acceptance Tests (Playwright)
- + All Tests
- + AI-Friendly Testing
- + Human-Friendly Testing
- + Environment Variables
- + Test Structure
- + Development Workflow
Commands
1 shared · 39 only in A · 7 only in B- − pnpm run setup
- − pnpm dev
- − pnpm build
- − 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 lint
- − 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 test:unit:watch
- + pnpm test:acceptance
- + pnpm test:acceptance:quiet
- + pnpm test:acceptance:headed
- + pnpm test:acceptance:report
- + pnpm test:slowmo
- + pnpm test
- pnpm test:unit
Section tags
3 shared · 11 only in A · 1 only in B- − build
- − lint-format
- − code-style
- − testing-strategy
- − git-pr
- − security
- − dependencies
- − database
- − ui
- − monorepo
- − docs
- + agent-behaviour
- setup
- test
- architecture
Line diff
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 · koenig/koenig-lexical/CLAUDE.md
@@ +1 @@
1# Koenig Lexical Test Guide
2
3## Test Commands
4
5### Unit Tests
6```bash
7pnpm test:unit # Run unit tests once
8pnpm test:unit:watch # Run unit tests in watch mode
9```
10
11### Acceptance Tests (Playwright)
12```bash
13pnpm test:acceptance # Run Playwright tests (headless, list reporter)
14pnpm test:acceptance:quiet # Minimal output, failures only
15pnpm test:acceptance:headed # Run with browser UI visible
16pnpm test:acceptance:report # Run with HTML report
17pnpm test:slowmo # Slow motion + UI
18```
19
20### All Tests
21```bash
22pnpm test # Run unit + acceptance tests, then lint
23```
24
25## AI-Friendly Testing
26
27The test runner has been configured to work well with AI agents:
28
29- **Default behavior**: Headless mode with list reporter (no browser UI, no web pages)
30- **Quiet mode**: Use `pnpm test:acceptance:quiet` for minimal output (only shows failures)
31- **Clean exit**: Tests complete without hanging processes or opening browsers
32- **Clear output**: List reporter provides clear pass/fail information
33
34## Human-Friendly Testing
35
36For debugging and development:
37
38- Use `pnpm test:acceptance:headed` to see the browser UI
39- Use `pnpm test:acceptance:report` to generate an HTML report
40- Use `pnpm test:slowmo` for slow-motion debugging
41
42## Environment Variables
43
44- `PLAYWRIGHT_HEADED=true` - Show browser UI
45- `PLAYWRIGHT_HTML_REPORT=true` - Generate HTML report
46- `PLAYWRIGHT_SLOWMO=100` - Slow motion delay (ms)
47
48## Test Structure
49
50- `test/unit/` - Unit tests (Vitest)
51- `test/e2e/` - Acceptance tests (Playwright, `test:acceptance` target)
52- `test/utils/` - Shared test utilities
53
54## Development Workflow
55
561. Run unit tests during development: `pnpm test:unit:watch`
572. Run acceptance tests before committing: `pnpm test:acceptance`
583. Use headed mode for debugging: `pnpm test:acceptance:headed`
59
@@ −1 +1 @@
1−# AGENTS.md
1+# Koenig Lexical Test Guide
22
3−This file provides guidance to AI Agents when working with code in this repository.
3+## Test Commands
44
5−## Package Manager
6−
7−**Always use `pnpm` for all commands.** This repository uses pnpm workspaces, not npm.
8−
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.
10−
11−## Monorepo Structure
12−
13−Ghost 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
21−Two 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
39−Merged 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−
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−
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`:
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`)
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.
76−
77−### packages/* - Shared workspace libraries
78−Backend 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
5+### Unit Tests
966 ```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)
7+pnpm test:unit # Run unit tests once
8+pnpm test:unit:watch # Run unit tests in watch mode
1009 ```
10110
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
11+### Acceptance Tests (Playwright)
10512 ```bash
106−pnpm build # Build all packages (Nx handles dependencies)
107−pnpm build:clean # Clean build artifacts and rebuild
13+pnpm test:acceptance # Run Playwright tests (headless, list reporter)
14+pnpm test:acceptance:quiet # Minimal output, failures only
15+pnpm test:acceptance:headed # Run with browser UI visible
16+pnpm test:acceptance:report # Run with HTML report
17+pnpm test:slowmo # Slow motion + UI
10818 ```
10919
110−### Testing
20+### All Tests
11121 ```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)
115−
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
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)
129−pnpm test:e2e # Run e2e/ Playwright tests
130−
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
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:
137−pnpm exec vitest -c vitest.config.db.ts test/integration/path/to/test.test.js
138−
139−# Ember Admin tests (from the repository root)
140−pnpm 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.
145−pnpm nx run ghost-admin:test -- 1 --file-path=tests/acceptance/editor/publish-flow-test.js
22+pnpm test # Run unit + acceptance tests, then lint
14623 ```
14724
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.
25+## AI-Friendly Testing
15426
155−### Linting
156−```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
160−```
27+The test runner has been configured to work well with AI agents:
16128
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−```
29+- **Default behavior**: Headless mode with list reporter (no browser UI, no web pages)
30+- **Quiet mode**: Use `pnpm test:acceptance:quiet` for minimal output (only shows failures)
31+- **Clean exit**: Tests complete without hanging processes or opening browsers
32+- **Clear output**: List reporter provides clear pass/fail information
16833
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−```
34+## Human-Friendly Testing
17535
176−### How `pnpm dev` works
36+For debugging and development:
17737
178−The `pnpm dev` command uses a **hybrid Docker + host development** setup:
38+- Use `pnpm test:acceptance:headed` to see the browser UI
39+- Use `pnpm test:acceptance:report` to generate an HTML report
40+- Use `pnpm test:slowmo` for slow-motion debugging
17941
180−**What runs in Docker:**
181−- Ghost Core backend (with hot-reload via mounted source)
182−- MySQL, Redis, Mailpit
183−- Caddy gateway/reverse proxy
42+## Environment Variables
18443
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
44+- `PLAYWRIGHT_HEADED=true` - Show browser UI
45+- `PLAYWRIGHT_HTML_REPORT=true` - Generate HTML report
46+- `PLAYWRIGHT_SLOWMO=100` - Slow motion delay (ms)
18847
189−**Setup:**
190−```bash
191−# Start Ghost backend, Admin, Portal, and Docker services
192−pnpm dev
48+## Test Structure
19349
194−# Add optional public apps (comments-ui, sodo-search, signup-form, admin-toolbar)
195−pnpm dev:public
50+- `test/unit/` - Unit tests (Vitest)
51+- `test/e2e/` - Acceptance tests (Playwright, `test:acceptance` target)
52+- `test/utils/` - Shared test utilities
19653
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
54+## Development Workflow
20055
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
206−
207−# Everything available
208−pnpm 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:**
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
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:**
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−/>
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−
278−See `apps/portal/src/components/pages/email-receiving-faq.js` for a canonical example of correct `Interpolate` usage.
279−
280−### Build Dependencies (Nx)
281−
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
288−
289−## CSS Architecture
290−
291−### TailwindCSS v4 Setup
292−
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";
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−
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−});
344−```
345−
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`.
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
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`
56+1. Run unit tests during development: `pnpm test:unit:watch`
57+2. Run acceptance tests before committing: `pnpm test:acceptance`
58+3. Use headed mode for debugging: `pnpm test:acceptance:headed`
39659
