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.
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
# Ghost core tests (from ghost/core/)
cd ghost/core
pnpm test:unit # 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