@tryghost/koenig-lexical
v2.3.0
Published
Ghost's Lexical-based rich text post editor
Maintainers
Keywords
Readme
Koenig - Lexical edition
Ghost editor, based on the Lexical framework.
The package is published as an ES module only. Consuming apps import the editor
from @tryghost/koenig-lexical and its stylesheet from
@tryghost/koenig-lexical/style.css.
Development
The editor can be run in two modes:
- standalone mode: demo version that runs without a dependency on Ghost
- integrated mode: integrated into Ghost Admin
Standalone mode
Run pnpm dev to start the editor in standalone mode for development on http://localhost:5173. This command generates a demo site from the index.html file, which renders the demo app in demo/demo.jsx.
Integrated mode
All Koenig packages are part of the Ghost monorepo workspace, so no linking is
needed — run pnpm dev:lexical from the monorepo root. That starts the normal
Ghost dev environment plus the editor's dev:integrated target, which rebuilds
the editor (and kg-default-nodes / kg-default-transforms) on every change. In
this mode Admin's dev server reads the editor's dist directly instead of
pre-bundling it.
Now, if you open a post in Ghost Admin (http://localhost:2368/ghost in the main checkout), it uses your local version of the editor. Changes to the editor reach Admin a few seconds later, once the editor has been rebuilt.
Specific card setup
Gif card
To see this card locally, create a .env.local file in the koenig-lexical root package with a Klipy API key:
VITE_KLIPY_API_KEY=xxxGet a Klipy key at https://partner.klipy.com
Bookmark & Embed cards
These cards make external web requests. Since the demo doesn't have a server to process these requests, we must fetch these resources on the front end. To do this we need to enable CORS, which is most easily done with a browser extension like 'Test CORS' for Chrome. Otherwise you will see blocked requests logging errors in the console. This can also be avoided by using test data directly without fetching via fetchEmbed.js.
Additional notes
Project structure
/src
The main module source. /src/index.js is the entry point for the exposed module and should export everything needed to use the module from an external app.
/demo
Used for developing/demoing the editor. Renders a blank editor with all features enabled.
Styling
CSS
Styling should be done using Tailwind classes where possible.
All styles are scoped under .koenig-lexical class to avoid clashes and keep styling as isolated as possible. PostCSS nesting support is present to make this easier.
- Styles located in
src/styles/are included in the final built module. - Styles located in
demo/*.cssare only used in the demo and will not be included in the built module.
When packaging the module, styles are built into a separate dist/style.css, which consuming apps import alongside the module.
Content fonts
A host can restyle the text a writer writes by setting these variables on an
ancestor of .koenig-lexical. Each falls back to the editor's own typography,
so a host that sets none sees no change.
| Variable | Applies to | Default |
| ----------------------------------- | --------------------------------------------------------------------------------- | ----------------------- |
| --kg-font-body | Paragraphs, lists, quotes, tables, the placeholder and callout, toggle and CTA text | Georgia |
| --kg-font-body-scale | Multiplies the body's font sizes | 1 |
| --kg-font-body-line-height | The body's line height | 1.6em |
| --kg-font-body-tracking | The body's letter spacing | inherited |
| --kg-font-heading | Headings, and the heading and subheading text of header, signup, product and toggle cards | Inter |
| --kg-font-heading-scale | Multiplies the H1–H6 font sizes | 1 |
| --kg-font-heading-tracking-scale | Multiplies the H1–H6 letter spacing | 1 |
| --kg-font-code-size | Inline code's font size, relative to the text around it | .8em |
| --kg-font-code-vertical-align | Inline code's vertical alignment; baseline suits code set at the body's size | middle |
In Tailwind, font-body, font-heading and text-body read the same variables.
Captions, buttons, link previews and the editor's own controls are not content
and always stay in Inter.
SVGs
SVGs can be imported as React components in the same way as create-react-app. Typically files are stored in src/assets/.
All imported files are processed/optimised via SVGO (see svgo.config.js for optimisation config) and included in the built JS file.
Testing
We use Vitest for unit tests and
Playwright for browser acceptance testing. Ghost's
end-to-end test suite lives in the repository's top-level
e2e/ directory.
Package tests live in test/unit/ and test/e2e/, with shared test helpers in
test/utils/.
pnpm testruns all tests and exitspnpm test:unitruns unit testspnpm test:unit:watchruns unit tests and starts a test watcher that re-runs tests on file changespnpm test:unit:watch --uiruns unit tests and opens a browser UI for exploring and re-running testspnpm test:acceptanceruns browser acceptance tests headlesslypnpm test:acceptance:headedruns acceptance tests with the browser visiblepnpm test:acceptance:reportgenerates an HTML reportpnpm test:acceptance:quietuses minimal output, showing failures onlypnpm test:acceptance --uiopens Playwright's UI mode in watch mode for exploring and re-running acceptance testspnpm test:slowmoruns headed acceptance tests with a 100ms delay between instructions (some tests may fail or time out due to the added delays)
Acceptance tests start their own Vite demo server on port 5185, separate from
Admin's development server on 5174. They never reuse an existing server or move
to another port if it is occupied. Set KOENIG_TEST_PORT when running tests from
multiple worktrees, for example KOENIG_TEST_PORT=5186 pnpm test:acceptance.
The port must be an integer between 1 and 65535.
Use pnpm dev:test to run the test demo manually on the same configurable port.
The acceptance runner requires that port to be free, so stop the manual server
before running tests. For collaboration development, pnpm dev:multiplayer
starts the demo and WebSocket server together; acceptance tests do not start the
unused WebSocket server.
The acceptance-test commands use these environment variables:
KOENIG_TEST_PORT=5186changes the demo server port.PLAYWRIGHT_HEADED=trueshows the browser UI.PLAYWRIGHT_HTML_REPORT=truegenerates an HTML report.PLAYWRIGHT_SLOWMO=100adds 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.:
pnpm test:unit:watch -t "buildCardMenu"
How to debug acceptance tests on CI
You can download the report in case of tests were failed. It can be found in the actions Summary in the Artifacts section.
To check traces, run command npx playwright show-trace trace.zip.
More information about traces can be found here https://playwright.dev/docs/trace-viewer
ESM in acceptance tests
Node enables ECMAScript modules if type: 'module' in package.json file. It leads to some restrictions:
- No require, exports, module.exports, __filename, __dirname
- Mandatory file extensions
- No require.extensions. It means we don't have control over the extensions list. Further will be a description of why this is important.
We can make file extension optional with --experimental-specifier-resolution
flag, which we use. But node is not recognized jsx extension.
It can be solved with node loaders, whereas
as they're still in experimental mode, there is no appropriate
implementation for this use case.
The same issue was raised in the babel repo, but the loader won't be added while node loaders are
in experimental mode.
We can add our loader implementation to solve the issue. Still, in reality, we shouldn't need real
JSX components in acceptance tests. It can be a situation when some constants locate in the jsx file. In this case,
we can move them to js file. If it is a problem in the future, we can add our implementation of the loader or
add an extension to all imports in the project.
Editor integration
There's a vitest vscode extension that lets you run and debug individual unit tests/groups directly inside vscode.
Shipping
Koenig packages are versioned and published as part of the Ghost monorepo's workspace package release process. See the Koenig shipping guide.
