AGENTS.md
AGENTS.mdAGENTS.mdroot
Quality
89/100
Scores the file, not the repository.Length
2,464 words
58 headings · 14 code blocksRepository
55k
— · pushed 0 days agoLast changed
3 days ago
First indexed 3 days ago.1# AGENTS.md23This file provides guidance to AI Agents when working with code in this repository.45## Package Manager67**Always use `pnpm` for all commands.** This repository uses pnpm workspaces, not npm.89Shared 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.1011## Monorepo Structure1213Ghost is a pnpm + Nx monorepo with four workspace groups:1415### ghost/* - Core Ghost packages16- **ghost/core** - Main Ghost application (Node.js/Express backend)17 - Core server: `ghost/core/core/server/`18 - Frontend rendering: `ghost/core/core/frontend/`1920### apps/* - React-based UI applications21Two categories of apps:2223**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`2829**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 themes3233**Foundation Libraries**:34- `admin-x-framework` - Shared API hooks, routing, utilities35- `admin-x-design-system` - Legacy design system (being phased out)36- `shade` - New design system (shadcn/ui + Radix UI + react-hook-form + zod)3738### koenig/* - Ghost editor (Koenig) packages39Merged from the former TryGhost/Koenig repo with full git history:4041- **koenig-lexical** - The Lexical-based rich text editor UI. Bundled into42 Ghost Admin at build time (`apps/ember-admin` copies its UMD build into admin43 assets; `apps/admin` imports it directly)44- **kg-*** - Editor support packages: server-side renderers and converters45 consumed by `ghost/core` (kg-default-nodes, kg-lexical-html-renderer,46 kg-html-to-lexical, ...) plus frontend helpers (kg-unsplash-selector)4748All Koenig packages resolve via `workspace:` — nothing in dev, CI, or the49release archive installs them from npm. They are published to npm for50external consumers only, automatically as part of the Ghost release lane51(see `publish_koenig_packages` in ci.yml).5253**Zero-build dev via the `source` export condition.** The `kg-*` libraries54consumed by `ghost/core` (and `packages/parse-email-address`) declare a `source`55condition in their `package.json` `exports` that points at the raw56`src/*.ts`, listed *before* `types`/`import`/`require`:5758```jsonc59".": {60 "source": "./src/index.ts", // dev/test: read raw TS61 "types": "./build/esm/index.d.ts",62 "import": "./build/esm/index.js",63 "require": "./build/cjs/index.js" // prod/published: compiled JS64}65```6667`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 change70in a `kg-*` package is picked up with **no `tsc` rebuild**. Production and the71published npm tarball run plain `node`, which ignores `source` and uses72`build/` — and `src/` is excluded from each package's `files` array, so it is73never shipped. When adding a new backend-consumed TS workspace package, copy74this `exports` shape (see `packages/parse-email-address`) so it works build-free75in dev from day one; keep the `^build` graph for `tsc`/type-checking and prod.7677### packages/* - Shared workspace libraries78Backend and shared libraries consumed via `workspace:` — not published to npm:7980- **i18n** - Centralized internationalization for all apps81- **parse-email-address** - Email address parsing (see the `source` export82 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 test86 helpers87- **_template** - Scaffold for new packages; excluded from the workspace8889### e2e/ - End-to-end tests90- Playwright-based E2E tests with Docker container isolation91- See `e2e/CLAUDE.md` for detailed testing guidance9293## Common Commands9495### Development96```bash97corepack enable pnpm # Enable corepack to use the correct pnpm version98pnpm run setup # First-time setup (installs deps + submodules + builds workspace packages)99pnpm dev # Start development (Docker backend + host frontend dev servers)100```101102> **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.103104### Building105```bash106pnpm build # Build all packages (Nx handles dependencies)107pnpm build:clean # Clean build artifacts and rebuild108```109110### Testing111```bash112# Unit tests (from root)113pnpm test:unit # Run all unit tests in all packages114pnpm test:watch # Watch mode — unified Vitest watcher (ghost/core + all apps)115116# Ghost core tests (from ghost/core/)117cd ghost/core118pnpm test:unit # Unit tests only (Vitest, run once)119pnpm test:watch # Watch mode — ghost/core unit tests only120pnpm test:integration # Integration tests121pnpm test:e2e # Server-side e2e suites (webhooks/server/frontend/api) — not browser122pnpm test:all # All test types123124# These run on sqlite with no extra services. The Redis/MinIO/S3 adapter suites125# 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.127128# E2E browser tests (from root)129pnpm test:e2e # Run e2e/ Playwright tests130131# Running a single test132cd ghost/core133pnpm test:single test/unit/path/to/test.test.js # routes test/unit/* → unit config, test/* → DB config134135# Watch a single DB-backed file (integration/e2e) — the default test:watch only136# 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.js138139# Ember Admin tests (from the repository root)140pnpm nx run ghost-admin:test141142# 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's144# trailing `--parallel` option before additional Ember Exam arguments.145pnpm nx run ghost-admin:test -- 1 --file-path=tests/acceptance/editor/publish-flow-test.js146```147148> **Always run Ember Admin tests through Nx.** Running `ember test` or149> `ember exam` directly from `apps/ember-admin` skips the dependency build150> graph and commonly fails in fresh worktrees with missing outputs such as151> `koenig-lexical.umd.js`, `@tryghost/admin-x-framework/hooks`, or152> `@tryghost/kg-converters`. For focused runs, use Ember Exam's `--file-path`153> as shown above rather than appending `--filter` to the package script.154155### Linting156```bash157pnpm lint # Lint all packages158cd ghost/core && pnpm lint # Lint Ghost core (server, shared, frontend, tests)159cd apps/ember-admin && pnpm lint # Lint Ember admin160```161162### Database163```bash164pnpm knex-migrator migrate # Run database migrations165pnpm 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```168169### Docker170```bash171pnpm docker:build # Build Docker images172pnpm docker:clean # Stop containers, remove volumes and local images173pnpm docker:down # Stop containers174```175176### How `pnpm dev` works177178The `pnpm dev` command uses a **hybrid Docker + host development** setup:179180**What runs in Docker:**181- Ghost Core backend (with hot-reload via mounted source)182- MySQL, Redis, Mailpit183- Caddy gateway/reverse proxy184185**What runs on host by default:**186- Admin, legacy Ember admin, Portal, and foundation library dev watchers187- Optional public UMD app watchers can be added when needed188189**Setup:**190```bash191# Start Ghost backend, Admin, Portal, and Docker services192pnpm dev193194# Add optional public apps (comments-ui, sodo-search, signup-form, admin-toolbar)195pnpm dev:public196197# Develop the Koenig editor against Ghost Admin (adds a koenig-lexical rebuild198# watcher + preview server; Admin loads the editor from your local build)199pnpm dev:lexical200201# With optional services (uses Docker Compose file composition)202pnpm dev:analytics # Include Tinybird analytics203pnpm dev:storage # Include MinIO S3-compatible object storage204pnpm dev:stripe # Include Stripe webhook forwarding205pnpm dev:full # Include analytics, storage, Stripe, and public app watchers206207# Everything available208pnpm dev:all #209```210211**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)219220## Architecture Patterns221222### Admin Apps Integration (Micro-Frontend)223224**Build Process:**2251. Admin-x React apps build to `apps/*/dist` using Vite2262. `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`228229**Runtime Loading:**230- Ember admin uses `AdminXComponent` to dynamically import React apps231- React components wrapped in Suspense with error boundaries232- Apps receive config via `additionalProps()` method233234### Public Apps Integration235236- 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 attributes239240### i18n Architecture241242**Centralized Translations:**243- Single source: `packages/i18n/locales/{locale}/{namespace}.json`244- Namespaces: `ghost`, `portal`, `signup-form`, `comments`, `search`245- 60+ supported locales246- Context descriptions: `packages/i18n/locales/context.json` — every key must have a non-empty description247248**Translation Workflow:**249```bash250pnpm --filter @tryghost/i18n translate # Extract keys from source, update all locale files + context.json251pnpm --filter @tryghost/i18n lint:translations # Validate interpolation variables across locales252```253254`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.255256**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="..." /> }}`261262**Correct pattern (using Interpolate):**263```jsx264import Interpolate from '@doist/react-interpolate';265266<Interpolate267 mapping={{ a: <a href={link} /> }}268 string={t('Could not sign in. <a>Click here to retry</a>')}269/>270```271272**Incorrect pattern (split sentences):**273```jsx274// BAD: translators cannot reorder "Click here to retry" relative to the first sentence275{t('Could not sign in.')} <a href={link}>{t('Click here to retry')}</a>276```277278See `apps/portal/src/components/pages/email-receiving-faq.js` for a canonical example of correct `Interpolate` usage.279280### Build Dependencies (Nx)281282Critical build order (Nx handles automatically):2831. `shade` + `admin-x-design-system` build2842. `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 build288289## CSS Architecture290291### TailwindCSS v4 Setup292293Ghost 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.294295### Entry Point296297`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-selector299- `@import "@tryghost/shade/styles.css"` which loads the Shade design system styles300301### Shade Styles302303`apps/shade/styles.css` uses **unlayered** Tailwind imports:304```css305@import "tailwindcss/theme.css";306@import "./preflight.css";307@import "tailwindcss/utilities.css";308@import "tw-animate-css";309@import "./tailwind.theme.css";310```311312**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.313314Theme 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`.315316### Critical Rule: Embedded Apps Must NOT Import Shade Independently317318Apps 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.319320### Public Apps321322Public-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.323324### Legacy Apps325326`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.327328## Code Guidelines329330### Commit Messages331When the user asks you to create a commit or draft a commit message, load and follow the `commit` skill from `.agents/skills/commit`.332333### ESLint Config334Source 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.335336Minimal example for a new admin React app (`apps/new-feature/eslint.config.js`):337338```js339import {reactAppConfig} from '@internal/cfg-eslint-react';340export default reactAppConfig({341 tailwindCssPath: `${import.meta.dirname}/../admin/src/index.css`,342 shadeRestricted: true343});344```345346Conventions: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`.354355### When Working on Admin UI356- **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`360361### When Working on Public UI362- **Edit:** `apps/portal`, `apps/comments-ui`, etc.363- **Translations:** Separate namespaces (`portal.json`, `comments.json`)364- **Build:** UMD bundles for CDN distribution365366### When Working on Backend367- **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/`373374### Design System Usage375- **New components:** Use `shade` (shadcn/ui-inspired)376- **Legacy:** `admin-x-design-system` (being phased out, avoid for new work)377378### 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/`383384## Troubleshooting385386### Build Issues387```bash388pnpm fix # Clean cache + node_modules + reinstall389pnpm build:clean # Clean build artifacts390pnpm nx reset # Reset Nx cache391```392393### Test Issues394- **E2E failures:** Check `e2e/CLAUDE.md` for debugging tips395- **Docker issues:** `pnpm docker:clean && pnpm docker:build`396
Also in TryGhost/Ghost
Diff this repo’s formatsOne repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| TryGhost/Ghostapps/shade/AGENTS.md · 55k | AGENTS.md | buildtestlint-formatstyle+6 | 96/100 | 3 days ago | |
| TryGhost/Ghoste2e/AGENTS.md · 55k | AGENTS.md | setupteststylearch+2 | 100/100 | 3 days ago | |
| TryGhost/Ghostkoenig/koenig-lexical/CLAUDE.md · 55k | CLAUDE.md | setuptestarchagent-behaviour | 78/100 | 3 days ago |
Similar configs
Same format, overlapping stack, ranked by quality.
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| n8n-io/n8npackages/@n8n/agents/AGENTS.md · 199k | AGENTS.md | buildteststylearch+3 | 100/100 | 3 days ago | |
| duckduckgo/content-scope-scriptsspecial-pages/AGENTS.md · 70 | AGENTS.md | buildteststylearch+3 | 100/100 | 3 days ago | |
| SkeneTechnologies/skene-cookbookAGENTS.md · 51 | AGENTS.md | setupbuildtestlint-format+7 | 100/100 | 2 days ago | |
| wpscanteam/wpscanAGENTS.md · 9.7k | AGENTS.md | setupbuildteststyle+6 | 100/100 | 2 days ago | |
| code-yeongyu/oh-my-openagentpackages/web/AGENTS.md · 67k | AGENTS.md | setupbuildtestlint-format+6 | 100/100 | 2 days ago | |
| mui/material-uiAGENTS.md · 99k | AGENTS.md | setupbuildtestlint-format+9 | 100/100 | 3 days ago | |
| TryGhost/Ghoste2e/AGENTS.md · 55k | AGENTS.md | setupteststylearch+2 | 100/100 | 3 days ago | |
| elastic/elasticsearchx-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/AGENTS.md · 78k | AGENTS.md | buildtestlint-formatstyle+2 | 100/100 | 3 days ago |
