This file provides guidance to AI Agents when working with code in this repository.
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.
Ghost is a pnpm + Nx monorepo with three workspace groups:
- ghost/core - Main Ghost application (Node.js/Express backend)
- Core server:
ghost/core/core/server/ - Frontend rendering:
ghost/core/core/frontend/
- Core server:
- ghost/admin - Ember.js admin client (legacy, being migrated to React)
- ghost/i18n - Centralized internationalization for all apps
Two categories of apps:
Admin Apps (embedded in Ghost Admin):
admin-x-settings,admin-x-activitypub- Settings and integrationsposts,stats- Post analytics and site-wide analytics- Built with Vite + React +
@tanstack/react-query
Public Apps (served to site visitors):
portal,comments-ui,signup-form,sodo-search,announcement-bar- Built as UMD bundles, loaded via CDN in site themes
Foundation Libraries:
admin-x-framework- Shared API hooks, routing, utilitiesadmin-x-design-system- Legacy design system (being phased out)shade- New design system (shadcn/ui + Radix UI + react-hook-form + zod)
- Playwright-based E2E tests with Docker container isolation
- See
e2e/CLAUDE.mdfor detailed testing guidance
corepack enable pnpm # Enable corepack to use the correct pnpm version
pnpm run setup # First-time setup (installs deps + submodules)
pnpm dev # Start development (Docker backend + host frontend dev servers)pnpm build # Build all packages (Nx handles dependencies)
pnpm build:clean # Clean build artifacts and rebuild# Unit tests (from root)
pnpm test:unit # Run all unit tests in all packages
pnpm test:watch # Watch mode — unified Vitest watcher (ghost/core + all apps)
# Ghost core tests (from ghost/core/)
cd ghost/core
pnpm test:unit # Unit tests only (Vitest, run once)
pnpm test:watch # Watch mode — ghost/core unit tests only
pnpm test:integration # Integration tests
pnpm test:e2e # E2E API tests (not browser)
pnpm test:all # All test types
# E2E browser tests (from root)
pnpm test:e2e # Run e2e/ Playwright tests
# Running a single test
cd ghost/core
pnpm test:single test/unit/path/to/test.test.jspnpm lint # Lint all packages
cd ghost/core && pnpm lint # Lint Ghost core (server, shared, frontend, tests)
cd ghost/admin && pnpm lint # Lint Ember adminpnpm knex-migrator migrate # Run database migrations
pnpm reset:data # Reset database with test data (1000 members, 100 posts) (requires pnpm dev running)
pnpm reset:data:empty # Reset database with no data (requires pnpm dev running)pnpm docker:build # Build Docker images
pnpm docker:clean # Stop containers, remove volumes and local images
pnpm docker:down # Stop containersThe pnpm dev command uses a hybrid Docker + host development setup:
What runs in Docker:
- Ghost Core backend (with hot-reload via mounted source)
- MySQL, Redis, Mailpit
- Caddy gateway/reverse proxy
What runs on host:
- Frontend dev servers (Admin, Portal, Comments UI, etc.) in watch mode with HMR
- Foundation libraries (shade, admin-x-framework, etc.)
Setup:
# Start everything (Docker + frontend dev servers)
pnpm dev
# With optional services (uses Docker Compose file composition)
pnpm dev:analytics # Include Tinybird analytics
pnpm dev:storage # Include MinIO S3-compatible object storage
pnpm dev:all # Include all optional servicesAccessing Services:
- Ghost:
http://localhost:2368(database:ghost_dev) - Mailpit UI:
http://localhost:8025(email testing) - MySQL:
localhost:3306 - Redis:
localhost:6379 - Tinybird:
http://localhost:7181(when analytics enabled) - MinIO Console:
http://localhost:9001(when storage enabled) - MinIO S3 API:
http://localhost:9000(when storage enabled)
Build Process:
- Admin-x React apps build to
apps/*/distusing Vite ghost/admin/lib/asset-deliverycopies them toghost/core/core/built/admin/assets/*- Ghost admin serves from
/ghost/assets/{app-name}/{app-name}.js
Runtime Loading:
- Ember admin uses
AdminXComponentto dynamically import React apps - React components wrapped in Suspense with error boundaries
- Apps receive config via
additionalProps()method
- 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
Centralized Translations:
- Single source:
ghost/i18n/locales/{locale}/{namespace}.json - Namespaces:
ghost,portal,signup-form,comments,search - 60+ supported locales
- Context descriptions:
ghost/i18n/locales/context.json— every key must have a non-empty description
Translation Workflow:
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 localestranslate 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:
- Never split sentences across multiple
t()calls. Translators cannot reorder words across separate keys. Instead, use@doist/react-interpolateto embed React elements (links, bold, etc.) within a single translatable string. - Always provide context descriptions. When adding a new key, add a description in
context.jsonexplaining where the string appears and what it does. CI will reject empty descriptions. - Use interpolation for dynamic values. Ghost uses
{variable}syntax:t('Welcome back, {name}!', {name: firstname}) - Use
<tag>syntax for inline elements. Combined with@doist/react-interpolate:t('Click <a>here</a> to retry')withmapping={{ a: <a href="..." /> }}
Correct pattern (using Interpolate):
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):
// 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.js for a canonical example of correct Interpolate usage.
Critical build order (Nx handles automatically):
shade+admin-x-design-systembuildadmin-x-frameworkbuilds (depends on #1)- Admin apps build (depend on #2)
ghost/adminbuilds (depends on #3, copies via asset-delivery)ghost/coreserves admin build
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 (posts, stats, activitypub, admin-x-settings, admin-x-design-system) are scanned from this single entry point.
apps/admin/src/index.css is the main CSS entry point. It contains:
@sourcedirectives that scan class usage in shade, posts, stats, activitypub, admin-x-settings, admin-x-design-system, and kg-unsplash-selector@import "@tryghost/shade/styles.css"which loads the Shade design system styles
apps/shade/styles.css uses unlayered Tailwind imports:
@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.
Apps consumed via @source (posts, stats, 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-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.
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.
When the user asks you to create a commit or draft a commit message, load and follow the commit skill from .agents/skills/commit.
- New features: Build in React (
apps/admin-x-*orapps/posts) - Use:
admin-x-frameworkfor API hooks (useBrowse,useEdit, etc.) - Use:
shadedesign system for new components (not admin-x-design-system) - Translations: Add to
ghost/i18n/locales/en/ghost.json
- Edit:
apps/portal,apps/comments-ui, etc. - Translations: Separate namespaces (
portal.json,comments.json) - Build: UMD bundles for CDN distribution
- 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/
- New components: Use
shade(shadcn/ui-inspired) - Legacy:
admin-x-design-system(being phased out, avoid for new work)
- 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/
pnpm fix # Clean cache + node_modules + reinstall
pnpm build:clean # Clean build artifacts
pnpm nx reset # Reset Nx cache- E2E failures: Check
e2e/CLAUDE.mdfor debugging tips - Docker issues:
pnpm docker:clean && pnpm docker:build