From 10371a9872287fc40e9abd1ed1899bb6643b623c Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Mon, 17 Aug 2026 11:58:07 +0100 Subject: [PATCH 1/5] Updated Koenig Lexical agent guidance (#30005) Koenig Lexical's agent guidance had become a second copy of its testing documentation. Keep shared commands, test layout, and Playwright configuration in the package README while retaining concise, agent-specific guidance for non-interactive test execution. Because package READMEs are published to npm, record the documentation update as a patch release. Clarify the same boundary in contributor guidance and the commit skill so published READMEs receive release intent while repository-only agent documentation does not. --- .agents/skills/commit/SKILL.md | 6 +++- .changeset/loose-pans-argue.md | 5 +++ .github/CONTRIBUTING.md | 4 +++ docs/contributing/workflow.md | 4 +++ koenig/koenig-lexical/AGENTS.md | 63 +++++++++------------------------ koenig/koenig-lexical/README.md | 9 +++++ 6 files changed, 43 insertions(+), 48 deletions(-) create mode 100644 .changeset/loose-pans-argue.md diff --git a/.agents/skills/commit/SKILL.md b/.agents/skills/commit/SKILL.md index 0825b057c45..c9716f87489 100644 --- a/.agents/skills/commit/SKILL.md +++ b/.agents/skills/commit/SKILL.md @@ -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 diff --git a/.changeset/loose-pans-argue.md b/.changeset/loose-pans-argue.md new file mode 100644 index 00000000000..2da772de325 --- /dev/null +++ b/.changeset/loose-pans-argue.md @@ -0,0 +1,5 @@ +--- +"@tryghost/koenig-lexical": patch +--- + +Updated Koenig Lexical testing documentation. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f96ce800a6d..01af2f04490 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -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). diff --git a/docs/contributing/workflow.md b/docs/contributing/workflow.md index 14f2397bd57..f18ee7ec1bc 100644 --- a/docs/contributing/workflow.md +++ b/docs/contributing/workflow.md @@ -63,6 +63,10 @@ Choose patch, minor, or major according to the package's public compatibility impact. The summary becomes the changelog entry, so describe the result for the package's consumers. +A package `README.md` is included when the package is published, so changing it +requires a release. Repository-only Markdown such as `AGENTS.md`, `CLAUDE.md`, +changelogs, and files under a package's `docs/` directory does not. + If a changed publishable package genuinely requires no release—for example, a test-only or internal tooling change—record that explicitly: diff --git a/koenig/koenig-lexical/AGENTS.md b/koenig/koenig-lexical/AGENTS.md index 5bb1242072f..b78a8d52cf7 100644 --- a/koenig/koenig-lexical/AGENTS.md +++ b/koenig/koenig-lexical/AGENTS.md @@ -1,26 +1,17 @@ -# Koenig Lexical Test Guide +# Koenig Lexical agent guidance -## Test Commands +Read the [`README.md`](./README.md), especially its development, testing, and +editor-integration sections, before changing this package. -### Unit Tests -```bash -pnpm test:unit # Run unit tests once -pnpm test:unit:watch # Run unit tests in watch mode -``` +## Required workflow -### Acceptance Tests (Playwright) -```bash -pnpm test:acceptance # Run Playwright tests (headless, list reporter) -pnpm test:acceptance:quiet # Minimal output, failures only -pnpm test:acceptance:headed # Run with browser UI visible -pnpm test:acceptance:report # Run with HTML report -pnpm test:slowmo # Slow motion + UI -``` - -### All Tests -```bash -pnpm test # Run unit + acceptance tests, then lint -``` +- Always use `pnpm`. +- These Playwright tests are package-level acceptance tests. Ghost's browser + E2E suite lives in the repository-level `e2e/` workspace. +- Use `pnpm test:unit:watch` for focused unit-test development and + `pnpm test:acceptance:quiet` when concise acceptance-test output is useful. +- Run the relevant focused tests while iterating, then run `pnpm test` and + `pnpm lint` before submitting changes. ## AI-Friendly Testing @@ -31,30 +22,8 @@ The test runner has been configured to work well with AI agents: - **Clean exit**: Tests complete without hanging processes or opening browsers - **Clear output**: List reporter provides clear pass/fail information -## Human-Friendly Testing - -For debugging and development: - -- Use `pnpm test:acceptance:headed` to see the browser UI -- Use `pnpm test:acceptance:report` to generate an HTML report -- Use `pnpm test:slowmo` for slow-motion debugging - -## Environment Variables - -- `PLAYWRIGHT_HEADED=true` - Show browser UI -- `PLAYWRIGHT_HTML_REPORT=true` - Generate HTML report -- `PLAYWRIGHT_SLOWMO=100` - Slow motion delay (ms) - -## Test Structure - -- `test/unit/` - Unit tests (Vitest) -- `test/e2e/` - Package-level acceptance tests (Playwright, - `test:acceptance` target); Ghost's end-to-end suite lives in the repository's - top-level `e2e/` directory -- `test/utils/` - Shared test utilities - -## Development Workflow - -1. Run unit tests during development: `pnpm test:unit:watch` -2. Run acceptance tests before committing: `pnpm test:acceptance` -3. Use headed mode for debugging: `pnpm test:acceptance:headed` +- Use `pnpm test:acceptance:headed`, `pnpm test:acceptance --ui`, or + `pnpm test:slowmo` only when interactive debugging is useful; do not leave a + browser or report server running after validation. +- Update the README when the package's shared commands or testing workflow + changes; do not duplicate that guidance here. diff --git a/koenig/koenig-lexical/README.md b/koenig/koenig-lexical/README.md index 2a42709e7e8..22bc5339d32 100644 --- a/koenig/koenig-lexical/README.md +++ b/koenig/koenig-lexical/README.md @@ -76,6 +76,9 @@ We use [Vitest](https://vitest.dev) for unit tests and end-to-end test suite lives in the repository's top-level [`e2e/`](../../e2e/) directory. +Package tests live in `test/unit/` and `test/e2e/`, with shared test helpers in +`test/utils/`. + - `pnpm test` runs all tests and exits - `pnpm test:unit` runs unit tests - `pnpm test:unit:watch` runs unit tests and starts a test watcher that re-runs tests on file changes @@ -89,6 +92,12 @@ end-to-end test suite lives in the repository's top-level - `pnpm test:slowmo` runs headed acceptance tests with a 100ms delay between instructions (some tests may fail or time out due to the added delays) +The acceptance-test commands use these environment variables: + +- `PLAYWRIGHT_HEADED=true` shows the browser UI. +- `PLAYWRIGHT_HTML_REPORT=true` generates an HTML report. +- `PLAYWRIGHT_SLOWMO=100` adds a delay in milliseconds between browser actions. + Before tests are started we build a version of the demo app that is used for the unit tests. When developing it can be useful to limit unit tests to specific keywords (taken from `describe` or `it/test` names). That's possible using the `-t` param and works with any of the above test commands, e.g.: From 89bde20c4183ee3b6eab657b57c9ece45068c994 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Mon, 17 Aug 2026 12:17:41 +0100 Subject: [PATCH 2/5] Expanded database codebase documentation (#30006) Document the database from its current schema, models, and services so contributors do not need to rely on stale Notion table descriptions. Add domain maps for members, newsletters, and email, including the distinction between send, batch, recipient, and delivery-failure records. Keep detailed fixture and default-settings guidance beside the schema code, where it is most likely to remain accurate. Clarify the boundaries between schema definitions, model behaviour, fixtures, default settings, and migrations so changes account for both new and existing installations. --- docs/codebase/database.md | 67 +++++++++++++++ ghost/core/core/server/data/schema/README.md | 87 ++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 ghost/core/core/server/data/schema/README.md diff --git a/docs/codebase/database.md b/docs/codebase/database.md index 88b47248324..b8ba833c38c 100644 --- a/docs/codebase/database.md +++ b/docs/codebase/database.md @@ -6,6 +6,9 @@ current shape expected after every migration has run. Bookshelf models in [`models/`](../../ghost/core/core/server/models/) define relationships and application behavior; do not infer those rules from the table shape alone. +The sections below are domain maps, not a replacement for the schema. They +explain why related tables exist and where to start when changing them. + ## Posts Post statuses are `draft`, `scheduled`, `published`, and `sent`. Sent posts are @@ -34,6 +37,68 @@ their referrer fields record the source, medium, and URL where available. The `source` on a member-created event records what created the member. Its values are `member`, `import`, `system`, `api`, and `admin`. +## Members + +`members` is the central record for a reader. It stores identity, status, +profile information, email engagement totals, and communication preferences. +Related data is separated by responsibility: + +| Responsibility | Tables | +| --- | --- | +| Labels | `labels`, `members_labels` | +| Custom fields | `members_custom_fields`, `members_custom_field_values` | +| Newsletter subscriptions | `newsletters`, `members_newsletters` | +| Tiers and access | `products`, `members_products`, `subscriptions` | +| Stripe state | `members_stripe_customers`, `members_stripe_customers_subscriptions`, `members_current_subscription`, `stripe_products`, `stripe_prices` | +| Offers | `offers`, `offer_redemptions` | +| Lifecycle and attribution history | the `members_*_events` tables | + +`labels` has a many-to-many relationship with `members` through +`members_labels`. Newsletter subscriptions use the same pattern through +`members_newsletters`. Custom-field definitions are stored once in +`members_custom_fields`; `members_custom_field_values` stores the values a +member has supplied. + +Ghost keeps a provider-independent subscription in `subscriptions`. The Stripe +tables cache the provider records needed to synchronize paid membership state. +`members_current_subscription` is the one-row-per-member lookup used to expose +the resolved Stripe subscription in Admin and member filtering. Read the member +model, the resolved-subscription view, and the Stripe service as well as the +schema before changing these relationships. + +The event tables are append-oriented history used for attribution, analytics, +and changes to member or subscription state. They are not alternative sources +of truth for the current member record. + +## Newsletters and email + +A newsletter send is represented across four main tables: + +| Table | Purpose | +| --- | --- | +| `emails` | One send for one post, including rendered content, recipient filter, aggregate counts, tracking options, and overall status | +| `email_batches` | Provider submissions for an email, including the member segment, provider identifier, status, and batch-level errors | +| `email_recipients` | The recipients selected for a send, their batch, a snapshot of member identity, and processing/delivery/open/failure timestamps | +| `email_recipient_failures` | Structured temporary or permanent delivery failure information for a recipient | + +This split answers three different questions: `emails` records what Ghost sent, +`email_batches` records how it was submitted to the provider, and +`email_recipients` records who was included and what happened to each delivery. +Do not reconstruct a historical recipient list from current member or segment +state; the recipient rows are the send-time record. + +`posts.newsletter_id` and `emails.newsletter_id` associate the content and send +with a newsletter. The `newsletters` table owns newsletter identity, sender +configuration, subscription defaults, and presentation settings. + +`email_spam_complaint_events` records complaint events associated with a member +and email. Member rows also cache aggregate engagement values for browsing and +filtering; the per-send and per-recipient tables remain the detailed record. + +Automated emails use a separate set of `automation_*`, +`welcome_email_automation_*`, and `automated_email_recipients` tables. They do +not create newsletter `emails` and `email_recipients` rows. + ## Link redirects and clicks `redirects` stores the source path, destination, and associated post or @@ -49,3 +114,5 @@ amount, and payment-provider links. For changing this structure, follow the [database migrations guide](../practices/database-migrations.md). Keep the schema definition, migration, exporter table lists, and integrity tests in sync. +For initial data and settings defaults, see the +[schema and default data guide](../../ghost/core/core/server/data/schema/README.md). diff --git a/ghost/core/core/server/data/schema/README.md b/ghost/core/core/server/data/schema/README.md new file mode 100644 index 00000000000..03a8b11ca60 --- /dev/null +++ b/ghost/core/core/server/data/schema/README.md @@ -0,0 +1,87 @@ +# Schema and default data + +This directory defines the database shape and the records a new Ghost database +starts with. + +## Sources of truth + +- `schema.js` is the final table and index structure expected after every + migration has run. +- `fixtures/fixtures.json` contains durable records and relationships required + by a new site, including roles, permissions, the owner, starter content, + tiers, and a newsletter. +- `default-settings/default-settings.json` defines the settings a site starts + with, grouped by responsibility. + +The initialization migrations create the tables and then use the fixture +manager to add missing fixture records. The settings model flattens the grouped +default-settings file and inserts settings missing from the database. Some +values, such as signing keys and secrets, are generated when the defaults are +populated rather than stored in the JSON file. + +Fixtures and default settings are different mechanisms. Fixtures create model +records and relationships; default settings describe rows in the `settings` +table. + +## Fixtures + +Fixture entries are added through their Bookshelf models. The fixture manager +checks for an existing record before inserting it, adds roles and the owner +before dependent records, and then creates the declared relationships. + +Use fixtures only for records every new Ghost installation requires. Sample +development data belongs in the data generator, not in `fixtures.json`. + +When changing fixtures: + +1. Update `fixtures/fixtures.json`. +2. Add a database migration when existing installations need the same change. +3. Update affected models, exporter lists, and tests. +4. Run the schema integrity test documented in the + [database migrations guide](../../../../../../docs/practices/database-migrations.md). + +## Default settings + +The top-level keys in `default-settings/default-settings.json` become setting +groups. Each setting has a `defaultValue` and `type`, and may also declare +validation and flags. The settings model adds the group and key when it +flattens the file. + +Supported setting types include `string`, `number`, `boolean`, `array`, and +`object`. Values are stored in the database in the representation expected by +the settings model and cache. + +The flags used by settings migrations are: + +- `PUBLIC`: identifies a setting intended for a public settings surface. +- `RO`: identifies a read-only setting. +- `PUBLIC,RO`: applies both flags. + +Flags are part of the setting's stored contract, but each API surface still +controls which settings it selects and how they may be changed. Check the +relevant endpoint and serializer rather than assuming the flag alone grants +access. + +The `core` group is restricted to internal access. Other groups organize +settings that are commonly read or edited together; they do not by themselves +make a setting public. + +When adding or changing a default setting: + +1. Update `default-settings/default-settings.json` for new installations. +2. Add a migration for existing installations. Use the settings migration + utilities rather than inserting a row by hand. +3. Update validation, API serializers, and tests when the setting's behaviour + requires it. +4. Run the schema integrity test. + +Do not put environment-specific configuration in default settings. Runtime +configuration belongs in Ghost's configuration system; see the +[configuration guide](../../../../../../docs/codebase/configuration.md). + +## Schema changes + +Changing `schema.js` alone does not update an existing database. Every schema +change needs a migration that moves an installed database to the new shape. +Follow the [database migrations guide](../../../../../../docs/practices/database-migrations.md) +for generation, iteration, testing, and review requirements. From 27e3bd3b659a4af3a9527a547004115d1f5e8a56 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Mon, 17 Aug 2026 12:54:39 +0100 Subject: [PATCH 3/5] Added theme compatibility documentation (#30009) Explain why compatibility is expressed through GScan rules rather than theme version declarations, and preserve the message-level policy that determines whether an issue informs, warns, permits installation, or blocks activation. Document the cross-repository workflow because changes to Ghost's theme contract must be represented in GScan before its dependency is updated in Ghost. This keeps new Handlebars helpers, bundled themes, and compatibility checks in sync. --- docs/README.md | 1 + docs/codebase/theme-compatibility.md | 107 +++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 docs/codebase/theme-compatibility.md diff --git a/docs/README.md b/docs/README.md index 053c6ccdf49..5d45f49e242 100644 --- a/docs/README.md +++ b/docs/README.md @@ -84,6 +84,7 @@ Codebase guides explain how the main systems fit together: - [Post analytics](codebase/post-analytics.md) - [Site UUID](codebase/site-uuid.md) - [Stripe flows](codebase/stripe-flows.md) +- [Theme compatibility](codebase/theme-compatibility.md) Practice and contributor guides explain how to make and verify changes: diff --git a/docs/codebase/theme-compatibility.md b/docs/codebase/theme-compatibility.md new file mode 100644 index 00000000000..8a0a55831c9 --- /dev/null +++ b/docs/codebase/theme-compatibility.md @@ -0,0 +1,107 @@ +# Theme compatibility + +Ghost themes use Handlebars templates and helpers provided by Ghost. When Ghost +adds a theme feature and a theme starts using it, that theme is not compatible +with older Ghost versions that do not provide the feature. When Ghost removes +or changes a theme feature, older themes may stop working on the new version. + +Incompatibility does not always produce a clear error. A feature may do nothing, +content may disappear, the output may look wrong, or a page may return an +error. People also commonly install the latest release of a theme on an older +Ghost version, or update Ghost without first checking their theme. + +[GScan](https://github.com/TryGhost/gscan) validates themes against the rules +for a Ghost major version. Ghost runs GScan when it loads or uploads a theme and +shows the results in Admin. Theme developers can also use +[gscan.ghost.org](https://gscan.ghost.org/) or the GScan command-line tool. + +## Why Ghost uses GScan + +Ghost originally relied on theme developers declaring the supported Ghost +version in `package.json`: + +```json +{ + "engines": { + "ghost": "^5.5.0" + } +} +``` + +That requires a theme developer to know which Ghost release introduced every +feature the theme uses and to keep the declaration current. In practice, the +version was often wrong and people still experienced broken or missing output. + +GScan moves that compatibility knowledge into rules. It can recognize features +that Ghost may add later, as well as features Ghost has removed or changed, and +give people a clear explanation of what changed and how to respond. Usually the +answer is to update Ghost or update the theme. + +## Compatibility messages + +GScan supports messages at four levels: + +- **Recommendation** provides information for theme developers. +- **Warning** gives advance notice that a feature will be removed or changed. +- **Error** identifies a change that may cause unexpected output. +- **Fatal error** identifies a change that will cause Ghost to return an error + while rendering a page. + +Most GScan messages are non-fatal errors. They are shown when a theme is +installed, but the user can choose to ignore them. Fatal errors prevent the +theme from being activated. + +Use a fatal error only when a theme would throw an error while rendering a page, +and introduce one only in a major Ghost version. Warnings are shown in Admin in +development, and when GScan is run directly, but are hidden in Admin in +production. + +## Changing the theme layer + +Changes to helpers, templates, `package.json` fields, assets, translations, or +rendered markup may need a corresponding GScan change. Before changing a public +theme contract: + +1. Decide which Ghost versions the old and new behaviour supports. +2. Add or update a rule in the appropriate GScan check and version spec. +3. Give the rule a clear description of what changed and how to fix it. +4. Test the rule in GScan, then release GScan. +5. Update the `gscan` dependency in `ghost/core/package.json` and run Ghost's + theme tests. Theme fixtures may also need updating. + +Version specs inherit the helpers and rules from the preceding major version. +Add new compatibility information to the spec for the first Ghost major that +uses it rather than rewriting an older version's contract. + +## Adding a Handlebars helper + +Theme-facing helpers live in +[`ghost/core/core/frontend/helpers/`](../../ghost/core/core/frontend/helpers/), +with unit tests in +[`ghost/core/test/unit/frontend/helpers/`](../../ghost/core/test/unit/frontend/helpers/). + +Adding the implementation is not enough. GScan must know the helper name or it +will report valid theme usage as an unknown helper. To add one: + +1. Add the helper and its unit tests in Ghost. +2. Add its name to `knownHelpers` in the current major-version spec in GScan, + with GScan tests where needed. +3. Release GScan and update `ghost/core/package.json` to that version. +4. Run the Ghost helper registration and GScan compatibility test: + + ```bash + pnpm --dir ghost/core test:unit \ + test/unit/frontend/services/theme-engine/handlebars/helpers.test.js + ``` + +The compatibility test compares theme-facing helper files with GScan's +`knownHelpers` list. A helper that is deliberately internal or experimental +must be explicitly excluded there with a reason. + +## Default themes + +[Casper](https://github.com/TryGhost/Casper) and +[Source](https://github.com/TryGhost/Source) are included in this repository as +Git submodules under `ghost/core/content/themes/`. Changes to Ghost's theme +contract must remain compatible with these themes, and the Ghost theme tests +must pass after a GScan update. From 21683784548bea0f0a26e9bfe764a474266722a2 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Mon, 17 Aug 2026 12:56:44 +0100 Subject: [PATCH 4/5] Moved root agent guidance into codebase docs (#30007) Shared architecture and development guidance had accumulated in the root AGENTS.md, making human-readable documentation harder to discover and allowing agent instructions to become an alternative source of truth. Move Admin integration and CSS guidance, ESLint conventions, and Ghost Core service patterns beside the code they describe. Keep AGENTS.md focused on task routing, execution constraints, and high-value warnings such as deploy skew, TypeScript-first services, and boot-owned initialization. --- AGENTS.md | 245 +++++----------------- apps/admin/README.md | 39 +++- configs/eslint/README.md | 47 +++++ docs/codebase/monorepo-structure.md | 9 + ghost/core/core/server/services/README.md | 30 +++ 5 files changed, 176 insertions(+), 194 deletions(-) create mode 100644 configs/eslint/README.md create mode 100644 ghost/core/core/server/services/README.md diff --git a/AGENTS.md b/AGENTS.md index 3c5d55fb57c..002fae02d6e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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: @@ -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:` 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 `