Koenig is Ghost's post editor. This directory contains the editor UI and its supporting packages, merged into the Ghost monorepo from the former TryGhost/Koenig repo with full git history.
Everything here resolves via workspace: specs — nothing in dev, CI, or the
release archive installs these packages from npm. The npm-published versions
exist for external consumers only (see Shipping).
| Package | What it is | Consumed by |
|---|---|---|
| koenig-lexical | The Lexical-based rich text editor (React). Ships as a UMD bundle with styles and SVGs inlined | ghost/admin (bundled into admin assets at build time), apps/admin, apps/admin-x-framework |
| kg-simplemde | Customised fork of SimpleMDE, used by koenig-lexical's markdown card | koenig-lexical |
| kg-unsplash-selector | React Unsplash image picker | koenig-lexical, apps/admin |
These are plain TypeScript Node libraries. kg-default-nodes is the single
source of truth for node rendering — both the editor and the server render
through it.
| Package | What it is | Consumed by |
|---|---|---|
| kg-default-nodes | Lexical node definitions for all of Ghost's cards, including each node's HTML renderer. Must stay browser-safe (it runs in the editor as well as on the server) | ghost/core, koenig-lexical, most other kg-* packages |
| kg-default-transforms | Lexical node transforms (denesting, blockquote children, etc.) shared between editor and server | koenig-lexical, kg-lexical-html-renderer, kg-html-to-lexical |
| kg-lexical-html-renderer | Renders a serialized Lexical state to front-end/email HTML server-side (not editor DOM — output differs per target, e.g. <table> markup for email) |
ghost/core |
| kg-html-to-lexical | Converts HTML strings into Lexical editor state (imports, API ?source=html) |
ghost/core |
| kg-converters | Converts between serialized Mobiledoc and Lexical formats | ghost/core, ghost/admin |
| Package | What it is | Consumed by |
|---|---|---|
| kg-clean-basic-html | Sanitises/normalises snippets of "basic HTML" (card captions etc.) | ghost/core, ghost/admin, kg-default-nodes |
| kg-markdown-html-renderer | Markdown → HTML rendering for the markdown card | ghost/core, kg-default-nodes, kg-default-cards |
| kg-utils | Small shared utilities (slugify) | kg-default-cards, kg-lexical-html-renderer, kg-markdown-html-renderer |
Still consumed by ghost/core to render posts that have never been converted
from Mobiledoc. Avoid new work here.
| Package | What it is | Consumed by |
|---|---|---|
| kg-card-factory | Card definition factory for the Mobiledoc renderer | ghost/core |
| kg-default-cards | Mobiledoc card definitions | ghost/core |
There is no linking or per-package install step — run pnpm setup once from
the monorepo root and everything resolves through the workspace.
Two modes:
- Standalone —
pnpm devinsidekoenig/koenig-lexicalstarts a demo app on http://localhost:5173 with all features enabled. Fastest feedback loop; no Ghost required. - Integrated —
pnpm dev:lexicalfrom the monorepo root starts the full Ghost dev environment plus a rebuild watcher for the editor (and kg-default-nodes / kg-default-transforms) with a preview server on port 4173. Ghost Admin at http://localhost:2368/ghost loads your local editor build; changes appear after the few seconds it takes to rebuild.
See koenig-lexical/README.md for card-specific setup (Klipy API key for the gif card, CORS notes for bookmark/embed cards) and styling conventions.
The libraries consumed by ghost/core declare a source export condition
pointing at their raw src/*.ts, listed before the compiled build/ entries.
ghost/core's dev runner and Vitest configs activate that condition, so a
source change in a kg-* package is picked up by a running Ghost dev server and
by core's tests with no tsc rebuild. Production and the published npm
tarballs ignore source and use build/.
You only need pnpm build (Nx handles the dependency order) for
type-checking, the browser lanes, and production artifacts — not for the
edit/run/test loop against ghost/core.
- kg-* Node libraries share a Vitest base config —
vitest.shared.ts (
createKoenigVitestConfig). Runpnpm test:unitin the package for unit tests,pnpm test:typesfor type checks, andpnpm lintfor lint.pnpm testruns the package's configured test suites and enforces coverage thresholds; packages with aposttesthook also run lint automatically. koenig-lexicalhas Vitest unit tests and a large Playwright browser acceptance suite:pnpm test:unit,pnpm test:acceptance(headless by default;:headed,:report, andtest:slowmovariants for debugging). See koenig-lexical/AGENTS.md. Ghost's end-to-end suite is the separate top-levele2e/workspace.kg-unsplash-selectoralso has a Playwright suite (pnpm test:acceptance).
Ghost itself never installs these packages from npm:
- Dev and CI resolve them from the workspace (
workspace:~). - Ghost Admin bundles the editor at build time:
ghost/admin/lib/asset-deliverycopieskoenig-lexical's UMD build into the admin assets, served at/ghost/assets/koenig-lexical/. - The release archive embeds the kg-* packages as component tarballs —
ghost/core/scripts/pack.jsdiscovers them transitively from ghost/core's dependencies and packs each one, so a deployed Ghost installs exactly the versions it was built with.src/is excluded from each package'sfilesarray, so raw source is never shipped.
The Publish Packages workflow runs
on stable Ghost release tags (no -rc prereleases), in parallel with Ghost's
own publishing workflow. Release commits consume pending changesets and commit
the resulting package versions before the tag is created.
The workflow runs
scripts/publish-packages.js, which:
- Selects every publishable workspace package, including the public Koenig packages.
- Builds the selected packages and their workspace dependencies.
- Publishes each committed version that is not already on npm, in dependency
order, rewriting
workspace:ranges to published versions.
For an out-of-band publish, the same workflow has a workflow_dispatch escape
hatch that can be narrowed to one package. The package version must already
have been bumped, normally by consuming a changeset; the workflow does not
calculate a new version itself.