{"owner":"TryGhost","repo":"Ghost","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md"],"skills":{"AGENTS.md":"# AGENTS.md\n\nThis file provides guidance to AI Agents when working with code in this repository.\n\nHuman-readable setup, workflow, testing, shipping, and architecture guidance\nlives in the [codebase documentation](docs/README.md). Treat those guides and\nnearby package READMEs as the source of truth for facts shared by humans and\nagents. This file adds agent-specific execution rules and code constraints.\n\nStart with:\n\n- [Development setup](docs/contributing/development-setup.md)\n- [Contribution workflow](docs/contributing/workflow.md)\n- [Testing](docs/contributing/testing.md)\n- [Shipping](docs/contributing/shipping.md)\n- [Monorepo structure](docs/codebase/monorepo-structure.md)\n\n## Package Manager\n\n**Always use `pnpm` for all commands.** This repository uses pnpm workspaces, not npm.\n\nShared 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.\n\n## Required Workflow\n\n- Run `pnpm setup` before other commands in a fresh checkout or worktree.\n- Use `pnpm check` as the default full validation command. Follow the\n  [testing guide](docs/contributing/testing.md) for focused commands and the\n  browser E2E and Ember Admin suites that run separately.\n- Read the nearest `AGENTS.md`, `CLAUDE.md`, and README files before changing a\n  package or subsystem. More specific instructions override this file.\n\n## Architecture Patterns\n\n### Admin Apps Integration (Micro-Frontend)\n\n**Build Process:**\n1. Admin-x React apps build to `apps/*/dist` using Vite\n2. `apps/ember-admin/lib/asset-delivery` copies them to `ghost/core/core/built/admin/assets/*`\n3. Ghost admin serves from `/ghost/assets/{app-name}/{app-name}.js`\n\n**Runtime Loading:**\n- Ember admin uses `AdminXComponent` to dynamically import React apps\n- React components wrapped in Suspense with error boundaries\n- Apps receive config via `additionalProps()` method\n\n### Public Apps Integration\n\n- Built as UMD bundles to `apps/*/umd/*.min.js`\n- Loaded via `<script>` tags in theme templates (injected by `{{ghost_head}}`)\n- Configuration passed via data attributes\n\n### i18n Architecture\n\n**Centralized Translations:**\n- Single source: `packages/i18n/locales/{locale}/{namespace}.json`\n- Namespaces: `ghost`, `portal`, `signup-form`, `comments`, `search`\n- 60+ supported locales\n- Context descriptions: `packages/i18n/locales/context.json` — every key must have a non-empty description\n\n**Translation Workflow:**\n```bash\npnpm --filter @tryghost/i18n translate          # Extract keys from source, update all locale files + context.json\npnpm --filter @tryghost/i18n lint:translations   # Validate interpolation variables across locales\n```\n\n`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.\n\n**Rules for Translation Keys:**\n1. **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.\n2. **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.\n3. **Use interpolation for dynamic values.** Ghost uses `{variable}` syntax: `t('Welcome back, {name}!', {name: firstname})`\n4. **Use `<tag>` syntax for inline elements.** Combined with `@doist/react-interpolate`: `t('Click <a>here</a> to retry')` with `mapping={{ a: <a href=\"...\" /> }}`\n\n**Correct pattern (using Interpolate):**\n```jsx\nimport Interpolate from '@doist/react-interpolate';\n\n<Interpolate\n    mapping={{ a: <a href={link} /> }}\n    string={t('Could not sign in. <a>Click here to retry</a>')}\n/>\n```\n\n**Incorrect pattern (split sentences):**\n```jsx\n// BAD: translators cannot reorder \"Click here to retry\" relative to the first sentence\n{t('Could not sign in.')} <a href={link}>{t('Click here to retry')}</a>\n```\n\nSee `apps/portal/src/components/pages/email-receiving-faq.jsx` for a canonical example of correct `Interpolate` usage.\n\n### Build Dependencies (Nx)\n\nCritical build order (Nx handles automatically):\n1. `shade` + `admin-x-design-system` build\n2. `admin-x-framework` builds (depends on #1)\n3. Admin apps build (depend on #2)\n4. `apps/ember-admin` builds (depends on #3, copies via asset-delivery)\n5. `ghost/core` serves admin build\n\n## CSS Architecture\n\n### TailwindCSS v4 Setup\n\nGhost Admin uses **TailwindCSS v4** via the `@tailwindcss/vite` plugin. CSS processing is centralized — only `apps/admin/vite.config.ts` loads the `@tailwindcss/vite` plugin. Embedded React apps (activitypub) are scanned from this single entry point alongside admin's own source.\n\n### Entry Point\n\n`apps/admin/src/index.css` is the main CSS entry point. It contains:\n- `@source` directives that scan class usage in shade, activitypub, admin-x-framework, and kg-unsplash-selector\n- `@import \"@tryghost/shade/styles.css\"` which loads the Shade design system styles\n\n### Shade Styles\n\n`apps/shade/styles.css` uses **unlayered** Tailwind imports:\n```css\n@import \"tailwindcss/theme.css\";\n@import \"./preflight.css\";\n@import \"tailwindcss/utilities.css\";\n@import \"tw-animate-css\";\n@import \"./tailwind.theme.css\";\n```\n\n**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.\n\nTheme 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`.\n\n### Critical Rule: Embedded Apps Must NOT Import Shade Independently\n\nApps consumed via `@source` (activitypub) 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.\n\n### Public Apps\n\nPublic-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.\n\n## Code Guidelines\n\n### Repository Skills\n\nRepository skills live in `.agents/skills/<skill-name>`. When adding a skill,\nalso add `.claude/skills/<skill-name>` as a symlink to\n`../../.agents/skills/<skill-name>` so Claude can discover the same canonical\nskill without duplicating it. Run `pnpm lint:agent-skills` to verify every\nrepository skill is linked correctly; CI runs the same check.\n\n### Commit Messages\nWhen the user asks you to create a commit or draft a commit message, load and follow the `commit` skill from `.agents/skills/commit`.\n\n### ESLint Config\nSource 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.\n\nMinimal example for a new admin React app (`apps/new-feature/eslint.config.js`):\n\n```js\nimport {reactAppConfig} from '@internal/cfg-eslint-react';\nexport default reactAppConfig({\n    tailwindCssPath: `${import.meta.dirname}/../admin/src/index.css`,\n    shadeRestricted: true\n});\n```\n\nConventions:\n- **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.\n- **Params prefixed `legacy*`** (`legacyTailwindV3ConfigPath`, `legacyJsTsSplit`) are escape hatches for migrations that haven't shipped yet. Intentional and visible — PRs to remove them are scoped.\n- **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`.\n- **Plugin deps**: a workspace must declare every eslint plugin its config resolves. Two cases:\n  - *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.\n  - *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).\n  - 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`.\n\n### When Working on Admin UI\n- **New features:** Build in React in `apps/admin` (domain folders under `src/`)\n- **Use:** `admin-x-framework` for API hooks (`useBrowse`, `useEdit`, etc.)\n- **Use:** `shade` design system for new components (not admin-x-design-system)\n- **Translations:** Add to `packages/i18n/locales/en/ghost.json`\n- **Deploy skew:** Ghost Admin and Ghost core deploy independently. New admin UI that depends on new settings, endpoints, or config must feature-detect backend support (e.g. settings-key presence in the browse response, as in `social-accounts.tsx`) and hide or no-op when absent. Labs flags alone are not a deploy-skew guard. Add an acceptance test for the “backend not deployed yet” case.\n\n### When Working on Public UI\n- **Edit:** `apps/portal`, `apps/comments-ui`, etc.\n- **Translations:** Separate namespaces (`portal.json`, `comments.json`)\n- **Build:** UMD bundles for CDN distribution\n\n### When Working on Backend\n- **Core logic:** `ghost/core/core/server/`\n- **Database Schema:** `ghost/core/core/server/data/schema/`\n- **API routes:** `ghost/core/core/server/api/`\n- **Services:** `ghost/core/core/server/services/`\n- **Models:** `ghost/core/core/server/models/`\n- **Frontend & theme rendering:** `ghost/core/core/frontend/`\n- **TypeScript by default:** New code under `ghost/core/core/server/services/` is TypeScript unless extending an existing JS module. Follow the gifts/donations pattern: domain logic as `.ts` with named exports; thin CJS `index.js` / `*-wrapper.js` only where boot/`require` still needs them.\n- **Service init:** New services get an explicit `init()` call from `ghost/core/core/boot.js` (same Promise.all as donations/gifts). Keep the wrapper’s `init()` idempotent so early callers are safe, but boot owns construction — not first request.\n\n### Design System Usage\n- **New components:** Use `shade` (shadcn/ui-inspired)\n- **Legacy:** `admin-x-design-system` (being phased out, avoid for new work)\n\n### Analytics (Tinybird)\n- **Local development:** `pnpm dev:analytics` (starts Tinybird + MySQL)\n- **Config:** Add Tinybird config to `ghost/core/config.development.json`\n- **Scripts:** `ghost/core/core/server/data/tinybird/scripts/`\n- **Datafiles:** `ghost/core/core/server/data/tinybird/`\n"},"files":{"AGENTS.md":"# AGENTS.md\n\nThis file provides guidance to AI Agents when working with code in this repository.\n\nHuman-readable setup, workflow, testing, shipping, and architecture guidance\nlives in the [codebase documentation](docs/README.md). Treat those guides and\nnearby package READMEs as the source of truth for facts shared by humans and\nagents. This file adds agent-specific execution rules and code constraints.\n\nStart with:\n\n- [Development setup](docs/contributing/development-setup.md)\n- [Contribution workflow](docs/contributing/workflow.md)\n- [Testing](docs/contributing/testing.md)\n- [Shipping](docs/contributing/shipping.md)\n- [Monorepo structure](docs/codebase/monorepo-structure.md)\n\n## Package Manager\n\n**Always use `pnpm` for all commands.** This repository uses pnpm workspaces, not npm.\n\nShared 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.\n\n## Required Workflow\n\n- Run `pnpm setup` before other commands in a fresh checkout or worktree.\n- Use `pnpm check` as the default full validation command. Follow the\n  [testing guide](docs/contributing/testing.md) for focused commands and the\n  browser E2E and Ember Admin suites that run separately.\n- Read the nearest `AGENTS.md`, `CLAUDE.md`, and README files before changing a\n  package or subsystem. More specific instructions override this file.\n\n## Architecture Patterns\n\n### Admin Apps Integration (Micro-Frontend)\n\n**Build Process:**\n1. Admin-x React apps build to `apps/*/dist` using Vite\n2. `apps/ember-admin/lib/asset-delivery` copies them to `ghost/core/core/built/admin/assets/*`\n3. Ghost admin serves from `/ghost/assets/{app-name}/{app-name}.js`\n\n**Runtime Loading:**\n- Ember admin uses `AdminXComponent` to dynamically import React apps\n- React components wrapped in Suspense with error boundaries\n- Apps receive config via `additionalProps()` method\n\n### Public Apps Integration\n\n- Built as UMD bundles to `apps/*/umd/*.min.js`\n- Loaded via `<script>` tags in theme templates (injected by `{{ghost_head}}`)\n- Configuration passed via data attributes\n\n### i18n Architecture\n\n**Centralized Translations:**\n- Single source: `packages/i18n/locales/{locale}/{namespace}.json`\n- Namespaces: `ghost`, `portal`, `signup-form`, `comments`, `search`\n- 60+ supported locales\n- Context descriptions: `packages/i18n/locales/context.json` — every key must have a non-empty description\n\n**Translation Workflow:**\n```bash\npnpm --filter @tryghost/i18n translate          # Extract keys from source, update all locale files + context.json\npnpm --filter @tryghost/i18n lint:translations   # Validate interpolation variables across locales\n```\n\n`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.\n\n**Rules for Translation Keys:**\n1. **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.\n2. **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.\n3. **Use interpolation for dynamic values.** Ghost uses `{variable}` syntax: `t('Welcome back, {name}!', {name: firstname})`\n4. **Use `<tag>` syntax for inline elements.** Combined with `@doist/react-interpolate`: `t('Click <a>here</a> to retry')` with `mapping={{ a: <a href=\"...\" /> }}`\n\n**Correct pattern (using Interpolate):**\n```jsx\nimport Interpolate from '@doist/react-interpolate';\n\n<Interpolate\n    mapping={{ a: <a href={link} /> }}\n    string={t('Could not sign in. <a>Click here to retry</a>')}\n/>\n```\n\n**Incorrect pattern (split sentences):**\n```jsx\n// BAD: translators cannot reorder \"Click here to retry\" relative to the first sentence\n{t('Could not sign in.')} <a href={link}>{t('Click here to retry')}</a>\n```\n\nSee `apps/portal/src/components/pages/email-receiving-faq.jsx` for a canonical example of correct `Interpolate` usage.\n\n### Build Dependencies (Nx)\n\nCritical build order (Nx handles automatically):\n1. `shade` + `admin-x-design-system` build\n2. `admin-x-framework` builds (depends on #1)\n3. Admin apps build (depend on #2)\n4. `apps/ember-admin` builds (depends on #3, copies via asset-delivery)\n5. `ghost/core` serves admin build\n\n## CSS Architecture\n\n### TailwindCSS v4 Setup\n\nGhost Admin uses **TailwindCSS v4** via the `@tailwindcss/vite` plugin. CSS processing is centralized — only `apps/admin/vite.config.ts` loads the `@tailwindcss/vite` plugin. Embedded React apps (activitypub) are scanned from this single entry point alongside admin's own source.\n\n### Entry Point\n\n`apps/admin/src/index.css` is the main CSS entry point. It contains:\n- `@source` directives that scan class usage in shade, activitypub, admin-x-framework, and kg-unsplash-selector\n- `@import \"@tryghost/shade/styles.css\"` which loads the Shade design system styles\n\n### Shade Styles\n\n`apps/shade/styles.css` uses **unlayered** Tailwind imports:\n```css\n@import \"tailwindcss/theme.css\";\n@import \"./preflight.css\";\n@import \"tailwindcss/utilities.css\";\n@import \"tw-animate-css\";\n@import \"./tailwind.theme.css\";\n```\n\n**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.\n\nTheme 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`.\n\n### Critical Rule: Embedded Apps Must NOT Import Shade Independently\n\nApps consumed via `@source` (activitypub) 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.\n\n### Public Apps\n\nPublic-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.\n\n## Code Guidelines\n\n### Repository Skills\n\nRepository skills live in `.agents/skills/<skill-name>`. When adding a skill,\nalso add `.claude/skills/<skill-name>` as a symlink to\n`../../.agents/skills/<skill-name>` so Claude can discover the same canonical\nskill without duplicating it. Run `pnpm lint:agent-skills` to verify every\nrepository skill is linked correctly; CI runs the same check.\n\n### Commit Messages\nWhen the user asks you to create a commit or draft a commit message, load and follow the `commit` skill from `.agents/skills/commit`.\n\n### ESLint Config\nSource 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.\n\nMinimal example for a new admin React app (`apps/new-feature/eslint.config.js`):\n\n```js\nimport {reactAppConfig} from '@internal/cfg-eslint-react';\nexport default reactAppConfig({\n    tailwindCssPath: `${import.meta.dirname}/../admin/src/index.css`,\n    shadeRestricted: true\n});\n```\n\nConventions:\n- **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.\n- **Params prefixed `legacy*`** (`legacyTailwindV3ConfigPath`, `legacyJsTsSplit`) are escape hatches for migrations that haven't shipped yet. Intentional and visible — PRs to remove them are scoped.\n- **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`.\n- **Plugin deps**: a workspace must declare every eslint plugin its config resolves. Two cases:\n  - *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.\n  - *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).\n  - 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`.\n\n### When Working on Admin UI\n- **New features:** Build in React in `apps/admin` (domain folders under `src/`)\n- **Use:** `admin-x-framework` for API hooks (`useBrowse`, `useEdit`, etc.)\n- **Use:** `shade` design system for new components (not admin-x-design-system)\n- **Translations:** Add to `packages/i18n/locales/en/ghost.json`\n- **Deploy skew:** Ghost Admin and Ghost core deploy independently. New admin UI that depends on new settings, endpoints, or config must feature-detect backend support (e.g. settings-key presence in the browse response, as in `social-accounts.tsx`) and hide or no-op when absent. Labs flags alone are not a deploy-skew guard. Add an acceptance test for the “backend not deployed yet” case.\n\n### When Working on Public UI\n- **Edit:** `apps/portal`, `apps/comments-ui`, etc.\n- **Translations:** Separate namespaces (`portal.json`, `comments.json`)\n- **Build:** UMD bundles for CDN distribution\n\n### When Working on Backend\n- **Core logic:** `ghost/core/core/server/`\n- **Database Schema:** `ghost/core/core/server/data/schema/`\n- **API routes:** `ghost/core/core/server/api/`\n- **Services:** `ghost/core/core/server/services/`\n- **Models:** `ghost/core/core/server/models/`\n- **Frontend & theme rendering:** `ghost/core/core/frontend/`\n- **TypeScript by default:** New code under `ghost/core/core/server/services/` is TypeScript unless extending an existing JS module. Follow the gifts/donations pattern: domain logic as `.ts` with named exports; thin CJS `index.js` / `*-wrapper.js` only where boot/`require` still needs them.\n- **Service init:** New services get an explicit `init()` call from `ghost/core/core/boot.js` (same Promise.all as donations/gifts). Keep the wrapper’s `init()` idempotent so early callers are safe, but boot owns construction — not first request.\n\n### Design System Usage\n- **New components:** Use `shade` (shadcn/ui-inspired)\n- **Legacy:** `admin-x-design-system` (being phased out, avoid for new work)\n\n### Analytics (Tinybird)\n- **Local development:** `pnpm dev:analytics` (starts Tinybird + MySQL)\n- **Config:** Add Tinybird config to `ghost/core/config.development.json`\n- **Scripts:** `ghost/core/core/server/data/tinybird/scripts/`\n- **Datafiles:** `ghost/core/core/server/data/tinybird/`\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# AGENTS.md\n\nThis file provides guidance to AI Agents when working with code in this repository.\n\nHuman-readable setup, workflow, testing, shipping, and architecture guidance\nlives in the [codebase documentation](docs/README.md). Treat those guides and\nnearby package READMEs as the source of truth for facts shared by humans and\nagents. This file adds agent-specific execution rules and code constraints.\n\nStart with:\n\n- [Development setup](docs/contributing/development-setup.md)\n- [Contribution workflow](docs/contributing/workflow.md)\n- [Testing](docs/contributing/testing.md)\n- [Shipping](docs/contributing/shipping.md)\n- [Monorepo structure](docs/codebase/monorepo-structure.md)\n\n## Package Manager\n\n**Always use `pnpm` for all commands.** This repository uses pnpm workspaces, not npm.\n\nShared 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.\n\n## Required Workflow\n\n- Run `pnpm setup` before other commands in a fresh checkout or worktree.\n- Use `pnpm check` as the default full validation command. Follow the\n  [testing guide](docs/contributing/testing.md) for focused commands and the\n  browser E2E and Ember Admin suites that run separately.\n- Read the nearest `AGENTS.md`, `CLAUDE.md`, and README files before changing a\n  package or subsystem. More specific instructions override this file.\n\n## Architecture Patterns\n\n### Admin Apps Integration (Micro-Frontend)\n\n**Build Process:**\n1. Admin-x React apps build to `apps/*/dist` using Vite\n2. `apps/ember-admin/lib/asset-delivery` copies them to `ghost/core/core/built/admin/assets/*`\n3. Ghost admin serves from `/ghost/assets/{app-name}/{app-name}.js`\n\n**Runtime Loading:**\n- Ember admin uses `AdminXComponent` to dynamically import React apps\n- React components wrapped in Suspense with error boundaries\n- Apps receive config via `additionalProps()` method\n\n### Public Apps Integration\n\n- Built as UMD bundles to `apps/*/umd/*.min.js`\n- Loaded via `<script>` tags in theme templates (injected by `{{ghost_head}}`)\n- Configuration passed via data attributes\n\n### i18n Architecture\n\n**Centralized Translations:**\n- Single source: `packages/i18n/locales/{locale}/{namespace}.json`\n- Namespaces: `ghost`, `portal`, `signup-form`, `comments`, `search`\n- 60+ supported locales\n- Context descriptions: `packages/i18n/locales/context.json` — every key must have a non-empty description\n\n**Translation Workflow:**\n```bash\npnpm --filter @tryghost/i18n translate          # Extract keys from source, update all locale files + context.json\npnpm --filter @tryghost/i18n lint:translations   # Validate interpolation variables across locales\n```\n\n`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.\n\n**Rules for Translation Keys:**\n1. **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.\n2. **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.\n3. **Use interpolation for dynamic values.** Ghost uses `{variable}` syntax: `t('Welcome back, {name}!', {name: firstname})`\n4. **Use `<tag>` syntax for inline elements.** Combined with `@doist/react-interpolate`: `t('Click <a>here</a> to retry')` with `mapping={{ a: <a href=\"...\" /> }}`\n\n**Correct pattern (using Interpolate):**\n```jsx\nimport Interpolate from '@doist/react-interpolate';\n\n<Interpolate\n    mapping={{ a: <a href={link} /> }}\n    string={t('Could not sign in. <a>Click here to retry</a>')}\n/>\n```\n\n**Incorrect pattern (split sentences):**\n```jsx\n// BAD: translators cannot reorder \"Click here to retry\" relative to the first sentence\n{t('Could not sign in.')} <a href={link}>{t('Click here to retry')}</a>\n```\n\nSee `apps/portal/src/components/pages/email-receiving-faq.jsx` for a canonical example of correct `Interpolate` usage.\n\n### Build Dependencies (Nx)\n\nCritical build order (Nx handles automatically):\n1. `shade` + `admin-x-design-system` build\n2. `admin-x-framework` builds (depends on #1)\n3. Admin apps build (depend on #2)\n4. `apps/ember-admin` builds (depends on #3, copies via asset-delivery)\n5. `ghost/core` serves admin build\n\n## CSS Architecture\n\n### TailwindCSS v4 Setup\n\nGhost Admin uses **TailwindCSS v4** via the `@tailwindcss/vite` plugin. CSS processing is centralized — only `apps/admin/vite.config.ts` loads the `@tailwindcss/vite` plugin. Embedded React apps (activitypub) are scanned from this single entry point alongside admin's own source.\n\n### Entry Point\n\n`apps/admin/src/index.css` is the main CSS entry point. It contains:\n- `@source` directives that scan class usage in shade, activitypub, admin-x-framework, and kg-unsplash-selector\n- `@import \"@tryghost/shade/styles.css\"` which loads the Shade design system styles\n\n### Shade Styles\n\n`apps/shade/styles.css` uses **unlayered** Tailwind imports:\n```css\n@import \"tailwindcss/theme.css\";\n@import \"./preflight.css\";\n@import \"tailwindcss/utilities.css\";\n@import \"tw-animate-css\";\n@import \"./tailwind.theme.css\";\n```\n\n**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.\n\nTheme 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`.\n\n### Critical Rule: Embedded Apps Must NOT Import Shade Independently\n\nApps consumed via `@source` (activitypub) 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.\n\n### Public Apps\n\nPublic-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.\n\n## Code Guidelines\n\n### Repository Skills\n\nRepository skills live in `.agents/skills/<skill-name>`. When adding a skill,\nalso add `.claude/skills/<skill-name>` as a symlink to\n`../../.agents/skills/<skill-name>` so Claude can discover the same canonical\nskill without duplicating it. Run `pnpm lint:agent-skills` to verify every\nrepository skill is linked correctly; CI runs the same check.\n\n### Commit Messages\nWhen the user asks you to create a commit or draft a commit message, load and follow the `commit` skill from `.agents/skills/commit`.\n\n### ESLint Config\nSource 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.\n\nMinimal example for a new admin React app (`apps/new-feature/eslint.config.js`):\n\n```js\nimport {reactAppConfig} from '@internal/cfg-eslint-react';\nexport default reactAppConfig({\n    tailwindCssPath: `${import.meta.dirname}/../admin/src/index.css`,\n    shadeRestricted: true\n});\n```\n\nConventions:\n- **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.\n- **Params prefixed `legacy*`** (`legacyTailwindV3ConfigPath`, `legacyJsTsSplit`) are escape hatches for migrations that haven't shipped yet. Intentional and visible — PRs to remove them are scoped.\n- **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`.\n- **Plugin deps**: a workspace must declare every eslint plugin its config resolves. Two cases:\n  - *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.\n  - *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).\n  - 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`.\n\n### When Working on Admin UI\n- **New features:** Build in React in `apps/admin` (domain folders under `src/`)\n- **Use:** `admin-x-framework` for API hooks (`useBrowse`, `useEdit`, etc.)\n- **Use:** `shade` design system for new components (not admin-x-design-system)\n- **Translations:** Add to `packages/i18n/locales/en/ghost.json`\n- **Deploy skew:** Ghost Admin and Ghost core deploy independently. New admin UI that depends on new settings, endpoints, or config must feature-detect backend support (e.g. settings-key presence in the browse response, as in `social-accounts.tsx`) and hide or no-op when absent. Labs flags alone are not a deploy-skew guard. Add an acceptance test for the “backend not deployed yet” case.\n\n### When Working on Public UI\n- **Edit:** `apps/portal`, `apps/comments-ui`, etc.\n- **Translations:** Separate namespaces (`portal.json`, `comments.json`)\n- **Build:** UMD bundles for CDN distribution\n\n### When Working on Backend\n- **Core logic:** `ghost/core/core/server/`\n- **Database Schema:** `ghost/core/core/server/data/schema/`\n- **API routes:** `ghost/core/core/server/api/`\n- **Services:** `ghost/core/core/server/services/`\n- **Models:** `ghost/core/core/server/models/`\n- **Frontend & theme rendering:** `ghost/core/core/frontend/`\n- **TypeScript by default:** New code under `ghost/core/core/server/services/` is TypeScript unless extending an existing JS module. Follow the gifts/donations pattern: domain logic as `.ts` with named exports; thin CJS `index.js` / `*-wrapper.js` only where boot/`require` still needs them.\n- **Service init:** New services get an explicit `init()` call from `ghost/core/core/boot.js` (same Promise.all as donations/gifts). Keep the wrapper’s `init()` idempotent so early callers are safe, but boot owns construction — not first request.\n\n### Design System Usage\n- **New components:** Use `shade` (shadcn/ui-inspired)\n- **Legacy:** `admin-x-design-system` (being phased out, avoid for new work)\n\n### Analytics (Tinybird)\n- **Local development:** `pnpm dev:analytics` (starts Tinybird + MySQL)\n- **Config:** Add Tinybird config to `ghost/core/config.development.json`\n- **Scripts:** `ghost/core/core/server/data/tinybird/scripts/`\n- **Datafiles:** `ghost/core/core/server/data/tinybird/`\n","category":"root","tokens":3005}]}