Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .agents/skills/commit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ Use this skill whenever the user asks you to create a git commit for the current
2. Only stage files relevant to the requested change. Do not include unrelated untracked files, generated files, or likely-local artifacts.
3. Read and follow `.github/CONTRIBUTING.md#commit-messages`. It is the
source of truth for Ghost's commit conventions.
4. Run `git status --short` after committing and confirm the result.
4. For publishable packages, check whether a release intent is required. A
package `README.md` is published and requires a release; repository-only
Markdown such as `AGENTS.md`, `CLAUDE.md`, changelogs, and package-local
`docs/` does not.
5. Run `git status --short` after committing and confirm the result.

## Important
- Do not push to remote unless the user explicitly asks
Expand Down
5 changes: 5 additions & 0 deletions .changeset/loose-pans-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tryghost/koenig-lexical": patch
---

Updated Koenig Lexical testing documentation.
4 changes: 4 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ This records which packages changed and the bump type (patch / minor / major); t
pnpm change --bump none
```

A package `README.md` is published with the package and requires a release.
Repository-only Markdown such as `AGENTS.md`, `CLAUDE.md`, changelogs, and
package-local `docs/` does not.

CI enforces this — the **Check app version bump** job fails a pull request that affects a publishable package without a covering changeset. The pre-commit hook prints a non-blocking reminder locally, and `pnpm change status` shows what's currently pending.

For more detail, see the [contribution workflow](../docs/contributing/workflow.md).
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,6 @@ jobs:
- '.github/workflows/ci.yml'
core:
- *shared
# Documentation and ownership metadata do not affect Ghost
# runtime behaviour, even when they live inside a project root.
- '!**/*.md'
- '!.github/CODEOWNERS'
- 'ghost/**'
- '!ghost/core/core/server/data/tinybird/**'
Expand All @@ -192,6 +189,10 @@ jobs:
- '!koenig/kg-unsplash-selector/**'
- '!koenig/kg-simplemde/**'
- '!koenig/*/test/**'
# Documentation does not affect Ghost runtime behaviour, even
# when it lives inside a project root. Keep this after every
# positive pattern so micromatch cannot add Markdown files back.
- '!**/*.md'
unit-test-globals:
- 'vitest.config.mjs'
core-unit-test-globals:
Expand Down
245 changes: 52 additions & 193 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
# AGENTS.md

This file provides guidance to AI Agents when working with code in this repository.

Human-readable setup, workflow, testing, shipping, and architecture guidance
lives in the [codebase documentation](docs/README.md). Treat those guides and
nearby package READMEs as the source of truth for facts shared by humans and
agents. This file adds agent-specific execution rules and code constraints.
Agent-specific execution guidance for the Ghost monorepo. Human-readable setup,
workflow, architecture, and practice guidance lives in the
[codebase documentation](docs/README.md) and nearby package READMEs.

Start with:

Expand All @@ -16,192 +13,54 @@ Start with:
- [Shipping](docs/contributing/shipping.md)
- [Monorepo structure](docs/codebase/monorepo-structure.md)

## Package Manager

**Always use `pnpm` for all commands.** This repository uses pnpm workspaces, not npm.

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.

## Required Workflow
## Required workflow

- Always use `pnpm`, never npm or Yarn. External dependency versions belong in
the catalogs in `pnpm-workspace.yaml`; workspace dependencies use
`workspace:` versions.
- Run `pnpm setup` before other commands in a fresh checkout or worktree.
- Use `pnpm check` as the default full validation command. Follow the
[testing guide](docs/contributing/testing.md) for focused commands and the
browser E2E and Ember Admin suites that run separately.
- Read the nearest `AGENTS.md`, `CLAUDE.md`, and README files before changing a
package or subsystem. More specific instructions override this file.

## Architecture Patterns

### Admin Apps Integration (Micro-Frontend)

**Build Process:**
1. Admin-x React apps build to `apps/*/dist` using Vite
2. `apps/ember-admin/lib/asset-delivery` copies them to `ghost/core/core/built/admin/assets/*`
3. Ghost admin serves from `/ghost/assets/{app-name}/{app-name}.js`

**Runtime Loading:**
- Ember admin uses `AdminXComponent` to dynamically import React apps
- React components wrapped in Suspense with error boundaries
- Apps receive config via `additionalProps()` method

### Public Apps Integration

- Built as UMD bundles to `apps/*/umd/*.min.js`
- Loaded via `<script>` tags in theme templates (injected by `{{ghost_head}}`)
- Configuration passed via data attributes

### i18n Architecture

**Centralized Translations:**
- Single source: `packages/i18n/locales/{locale}/{namespace}.json`
- Namespaces: `ghost`, `portal`, `signup-form`, `comments`, `search`
- 60+ supported locales
- Context descriptions: `packages/i18n/locales/context.json` — every key must have a non-empty description

**Translation Workflow:**
```bash
pnpm --filter @tryghost/i18n translate # Extract keys from source, update all locale files + context.json
pnpm --filter @tryghost/i18n lint:translations # Validate interpolation variables across locales
```

`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.

**Rules for Translation Keys:**
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.
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.
3. **Use interpolation for dynamic values.** Ghost uses `{variable}` syntax: `t('Welcome back, {name}!', {name: firstname})`
4. **Use `<tag>` syntax for inline elements.** Combined with `@doist/react-interpolate`: `t('Click <a>here</a> to retry')` with `mapping={{ a: <a href="..." /> }}`

**Correct pattern (using Interpolate):**
```jsx
import Interpolate from '@doist/react-interpolate';

<Interpolate
mapping={{ a: <a href={link} /> }}
string={t('Could not sign in. <a>Click here to retry</a>')}
/>
```

**Incorrect pattern (split sentences):**
```jsx
// BAD: translators cannot reorder "Click here to retry" relative to the first sentence
{t('Could not sign in.')} <a href={link}>{t('Click here to retry')}</a>
```

See `apps/portal/src/components/pages/email-receiving-faq.jsx` for a canonical example of correct `Interpolate` usage.

### Build Dependencies (Nx)

Critical build order (Nx handles automatically):
1. `shade` + `admin-x-design-system` build
2. `admin-x-framework` builds (depends on #1)
3. Admin apps build (depend on #2)
4. `apps/ember-admin` builds (depends on #3, copies via asset-delivery)
5. `ghost/core` serves admin build

## CSS Architecture

### TailwindCSS v4 Setup

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. Embedded React apps (activitypub) are scanned from this single entry point alongside admin's own source.

### Entry Point

`apps/admin/src/index.css` is the main CSS entry point. It contains:
- `@source` directives that scan class usage in shade, activitypub, admin-x-framework, and kg-unsplash-selector
- `@import "@tryghost/shade/styles.css"` which loads the Shade design system styles

### Shade Styles

`apps/shade/styles.css` uses **unlayered** Tailwind imports:
```css
@import "tailwindcss/theme.css";
@import "./preflight.css";
@import "tailwindcss/utilities.css";
@import "tw-animate-css";
@import "./tailwind.theme.css";
```

**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.

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`.

### Critical Rule: Embedded Apps Must NOT Import Shade Independently

Apps 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.

### Public Apps

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.

## Code Guidelines

### Repository Skills

Repository skills live in `.agents/skills/<skill-name>`. When adding a skill,
also add `.claude/skills/<skill-name>` as a symlink to
`../../.agents/skills/<skill-name>` so Claude can discover the same canonical
skill without duplicating it. Run `pnpm lint:agent-skills` to verify every
repository skill is linked correctly; CI runs the same check.

### Commit Messages
When the user asks you to create a commit or draft a commit message, load and
follow the `commit` skill from `.agents/skills/commit`. Read the canonical
[commit message guidelines](.github/CONTRIBUTING.md#commit-messages), and apply
the subject convention carefully to PR titles and proposed squash commits.
Local hook notices on intermediate commits are best-effort guidance.

### ESLint Config
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.

Minimal example for a new admin React app (`apps/new-feature/eslint.config.js`):

```js
import {reactAppConfig} from '@internal/cfg-eslint-react';
export default reactAppConfig({
tailwindCssPath: `${import.meta.dirname}/../admin/src/index.css`,
shadeRestricted: true
});
```

Conventions:
- **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.
- **Params prefixed `legacy*`** (`legacyTailwindV3ConfigPath`, `legacyJsTsSplit`) are escape hatches for migrations that haven't shipped yet. Intentional and visible — PRs to remove them are scoped.
- **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`.
- **Plugin deps**: a workspace must declare every eslint plugin its config resolves. Two cases:
- *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.
- *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).
- 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`.

### When Working on Admin UI
- **New features:** Build in React in `apps/admin` (domain folders under `src/`)
- **Use:** `admin-x-framework` for API hooks (`useBrowse`, `useEdit`, etc.)
- **Use:** `shade` design system for new components (not admin-x-design-system)
- **Translations:** Add to `packages/i18n/locales/en/ghost.json`
- **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.

### When Working on Public UI
- **Edit:** `apps/portal`, `apps/comments-ui`, etc.
- **Translations:** Separate namespaces (`portal.json`, `comments.json`)
- **Build:** UMD bundles for CDN distribution

### When Working on Backend
- **Core logic:** `ghost/core/core/server/`
- **Database Schema:** `ghost/core/core/server/data/schema/`
- **API routes:** `ghost/core/core/server/api/`
- **Services:** `ghost/core/core/server/services/`
- **Models:** `ghost/core/core/server/models/`
- **Frontend & theme rendering:** `ghost/core/core/frontend/`
- **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.
- **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.

### Design System Usage
- **New components:** Use `shade` (shadcn/ui-inspired)
- **Legacy:** `admin-x-design-system` (being phased out, avoid for new work)

### Analytics (Tinybird)
- **Local development:** `pnpm dev:analytics` (starts Tinybird + MySQL)
- **Config:** Add Tinybird config to `ghost/core/config.development.json`
- **Scripts:** `ghost/core/core/server/data/tinybird/scripts/`
- **Datafiles:** `ghost/core/core/server/data/tinybird/`
- Use `pnpm check` as the default full validation command. Browser E2E and Ember
Admin tests run separately; follow the testing guide.
- Read the nearest `AGENTS.md`, `CLAUDE.md`, and README before changing a package
or subsystem. More specific guidance overrides this file.
- When committing, load and follow `.agents/skills/commit/SKILL.md`.

## Repository skills

Repository skills live under `.agents/skills/`. When adding one, also add the
matching `.claude/skills/<name>` symlink to
`../../.agents/skills/<name>`. Run `pnpm lint:agent-skills` to verify discovery.

Use the relevant repository skill before adding an Admin API endpoint, database
migration, private feature flag, Shade component, or internal package.

## Task routing and important warnings

- **Admin UI:** read [`apps/admin/README.md`](apps/admin/README.md) and
[`apps/shade/AGENTS.md`](apps/shade/AGENTS.md). Build new features in React,
use `admin-x-framework` for APIs, and use Shade for UI. Admin and Core deploy
independently, so feature-detect backend support and test the older-backend
case.
- **Embedded Admin CSS:** do not import `@tryghost/shade/styles.css` from an
embedded app. Admin owns the single Tailwind and Shade CSS lane.
- **Translations:** follow the
[internationalization guide](docs/practices/internationalization.md). Run the
extraction command after changing `t()` calls and never split one sentence
across translation calls.
- **Public apps:** read the app's README and the
[shipping guide](docs/contributing/shipping.md). Their release and CSS lanes
differ from Admin.
- **Ghost Core:** use the [server map](docs/codebase/monorepo-structure.md#ghost-core)
and read the [services guide](ghost/core/core/server/services/README.md) before
adding a service. New standalone services use TypeScript; keep CommonJS only
at existing `require()` boundaries. Boot owns service initialization; do not
initialize on the first request.
- **ESLint:** use the shared factories and dependency rules in the
[ESLint configuration README](configs/eslint/README.md). A hand-written config
must declare every plugin it imports locally.
- **Analytics:** start with `pnpm dev:analytics` and follow the nearby Tinybird
READMEs under `ghost/core/core/server/data/tinybird/`.

Keep shared facts in human documentation. This file should contain only routing,
agent execution constraints, and high-value warnings that prevent recurring
mistakes.
39 changes: 38 additions & 1 deletion apps/admin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,51 @@ Uses an **Ember Bridge** system for smooth migration:
- Unported routes fall back to the existing Ember admin
- Both share the same UI space seamlessly

The React application uses `admin-x-framework` for API hooks, routing, and the
bridge to Ember. Shade provides its application wrapper and design system.
Embedded React applications are built before Ember Admin; Ember's asset-delivery
addon copies their production output and the Admin assets into
`ghost/core/core/built/admin/` for Ghost Core to serve.

### CSS

`src/index.css` is the single Tailwind CSS entry point for Admin. It imports
Shade's styles and uses `@source` directives to scan Admin, Shade, ActivityPub,
Admin Framework, and the embedded Koenig selector. Only this app loads the
`@tailwindcss/vite` plugin for the embedded Admin CSS lane.

Embedded Admin apps must not import `@tryghost/shade/styles.css` themselves.
Doing so generates duplicate utilities and creates cascade conflicts with
Ember's legacy CSS.

Shade's Tailwind imports are unlayered because Ember's legacy CSS is also
unlayered. This lets source order resolve overlapping utilities. Do not move
Shade's imports into a CSS layer without accounting for the legacy cascade.

### Deploy compatibility

Ghost Admin and Ghost Core can deploy at different times. New Admin UI that
depends on a new setting, endpoint, or configuration value must detect backend
support and hide or safely disable the feature when it is absent. A Labs flag
alone is not a compatibility check because the flag may exist before the
supporting backend version is live.

Add an acceptance test for the older-backend case. The social accounts settings
and membership tiers tests contain current examples of hiding controls until
their supporting settings are present.

## Development

```bash
# Start development server (from monorepo root)
pnpm dev
```

Build new Admin features in this React app. Use `admin-x-framework` for API
access and Shade for UI rather than adding new `admin-x-design-system`
components. Product copy belongs in the `ghost` namespace; follow the
[internationalization guide](../../docs/practices/internationalization.md).

## Testing

- **Unit tests** (`pnpm test:unit`): Vitest + jsdom, colocated `*.test.ts(x)` files.
Expand All @@ -30,4 +68,3 @@ pnpm nx run @tryghost/admin:build
```

This outputs to `apps/admin/dist/` and updates the assets in `ghost/core/core/built/admin/`.

Loading
Loading