simply007 / ckeditor-studio-bundle
CKEditor as the rich text editor in Pimcore Studio: classic and inline editors, configurable toolbar, optional CKEditor AI and real-time collaboration. Community package, not affiliated with Pimcore GmbH.
Package info
github.com/Simply007/pimcore-studio-ckeditor-bundle
Language:TypeScript
Type:pimcore-bundle
pkg:composer/simply007/ckeditor-studio-bundle
Requires
- php: >=8.3
- pimcore/pimcore: ^2026.1
- pimcore/studio-ui-bundle: ^2026.3
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-10 10:26:15 UTC
README
CKEditor rich text editor mounted as the wysiwyg editor in Pimcore Studio, the
CKEditor counterpart to pimcore/quill-bundle, whose 2026.x architecture this bundle
mirrors. It started as the demo for the talk "AI Best Practices for Your Product" at
Pimcore Community Dev Days 2026 and was extracted from the
guidepost demo repository,
which stays the reference installation.
Important
Proof of concept. This bundle is a showcase built for Pimcore Community Dev Days 2026. It has not been reviewed or verified by Pimcore. Treat it as a starting point, not a production-ready integration, until Pimcore verifies it.
Community package: not an official Pimcore or CKEditor product. Supported line: Pimcore 2026.3.x with Pimcore Studio 2026.3.x.
Status: the core integration is runtime-tested end to end on the demo stack (see
TESTING.md); the runtime configuration described below is build-tested and
statically verified. The premium feature path (CKEditor AI, real-time collaboration,
with the bundle's own Cloud Services token endpoint) is loaded on demand behind
dynamic imports and was verified live in Studio with a trial key (see "Testing
premium locally"). Experiment log on the agent skills used to build it: see
SKILL-FINDINGS.md.
What it does
- Replaces the Quill editor with CKEditor for every wysiwyg surface in Pimcore Studio: data object wysiwyg fields and document wysiwyg editables (both render through the same registry component).
- Classic editor, open source build (npm packages
ckeditor5+@ckeditor/ckeditor5-react), with license-key gating for premium use (see below). - Asset/document/object drag and drop inserts images and links with
pimcore_id/pimcore_typeattributes;GeneralHtmlSupportkeeps those attributes (plus class/id/style) in the data.
How it works
Pimcore Studio is a module-federation host. This bundle contributes a remote:
- The Symfony bundle (
Simply007\CKEditorStudioBundle) registers aWebpackEntryPointProvider(tagpimcore_studio_ui.webpack_entry_point_provider, plus the document editor iframe tag) that points Studio at the committed rsbuild output inpublic/studio/build/<build-id>/entrypoints.json. - The frontend (
assets/studio/, rsbuild + module federation, dev server port 3033) exposes a Studio plugin. On startup it grabs theComponentRegistryfrom the Studio SDK container and callscomponentRegistry.override({ component, name: componentConfig.wysiwyg.editor.name }): the singlewysiwyg.editorcomponent slot now renders CKEditor. - The replacement component implements Studio's
WysiwygProps/WysiwygEditorRefcontract (value/onChange, drag and drop hook, maxCharacters), so Studio core treats it exactly like the built-in editor. - Runtime configuration travels the other way: a
StudioAppConfigProvider(tagpimcore_studio_ui.app_config_provider) merges ackeditorStudiokey into thedata-app-configJSON on the Studio shell page, and the editor reads it back throughappConfigfrom@pimcore/studio-ui-bundle/app. Config changes therefore need only acache:clear, never a frontend rebuild.
Install and enable
The package is on Packagist as
simply007/ckeditor-studio-bundle.
Inside the PHP container (host PHP often lacks Pimcore's extensions):
# 1. Composer
composer require simply007/ckeditor-studio-bundle
// 2. config/bundles.php: register the bundle Simply007\CKEditorStudioBundle\CKEditorStudioBundle::class => ['all' => true],
# 3. Publish assets and clear cache
bin/console assets:install public --symlink --relative
bin/console cache:clear
Other editor bundles (for example PimcoreQuillBundle) can stay enabled; pick the
editor with ckeditor_studio.editor (see "Editor selection") and open Studio: wysiwyg
fields render the selected editor.
Configuration
All configuration lives under the ckeditor_studio key, for example in
config/packages/ckeditor_studio.yaml. Everything is optional; with no config at all
the editor runs the open source feature set with the default toolbar.
Editor selection
Several wysiwyg editor bundles can stay enabled side by side; one value decides which editor Studio uses:
ckeditor_studio: editor: ckeditor # ckeditor (default) | quill | default
ckeditor: this bundle's editor.quill: the editor another bundle registered (PimcoreQuillBundle); when no other editor bundle is enabled, it falls back to CKEditor with a console warning.default: Studio's built-in editor.
Switching needs only cache:clear (the value reaches the browser through the app
config, no rebuild). How it works: Studio's component registry has no arbitration
(the last override() of the wysiwyg.editor slot wins), but its order is
deterministic: Studio registers its built-in editor first, then plugin modules
initialize in ascending plugin priority. This bundle ships two plugins, a probe at
priority -1000 that records Studio's built-in editor before any editor bundle runs,
and the selector at priority 1000 that runs after every other editor bundle and
applies the configured choice. Applies to data object fields and document editables
(the editmode iframe bootstraps plugins the same way).
License key gating
- No key configured (default): the editor runs with
licenseKey: 'GPL'and the current free feature set. The "Powered by CKEditor" logo is part of that deal. - Key configured: it is passed to CKEditor as
licenseKeyand unlocks the premium gates below. The simplest route is the environment variable the config defaults to:
# .env.local (never commit real keys)
CKEDITOR_LICENSE_KEY=your-key-from-portal.ckeditor.com
or explicitly in yaml:
ckeditor_studio: license_key: '%env(CKEDITOR_LICENSE_KEY)%'
The key reaches the browser through the Studio shell page. That is how CKEditor licensing works everywhere (the key is validated client side and is not a secret in the server sense), but keep genuinely secret values, such as an AI provider API key, on the server.
Toolbar
The toolbar is a plain list of CKEditor toolbar items, in the spirit of TYPO3 RTE presets or Drupal editor config. The default preset is:
ckeditor_studio: toolbar: - undo - redo - '|' - heading - '|' - bold - italic - underline - '|' - alignment - '|' - bulletedList - numberedList - outdent - indent - '|' - blockQuote - link - insertTable - '|' - sourceEditing - fullscreen
Change it by overriding the whole list; '|' is a separator. A minimal preset:
ckeditor_studio: toolbar: ['undo', 'redo', '|', 'bold', 'italic', 'link']
Items must be backed by a plugin the frontend build ships (currently: Essentials,
Paragraph, Heading, Bold, Italic, Underline, Alignment, Link, List, Indent,
BlockQuote, Table, Image, ImageResize, SourceEditing, GeneralHtmlSupport, Fullscreen).
fullscreen (free, part of the open source package) expands the editor over the
whole Studio window in a page-style layout and returns cleanly. With the premium
gates on, fullscreen carries the CKEditor AI panel and the connected users list
along, so AI keeps working there (verified live in Studio).
Data object fields without a configured height are fitted to Studio's panel (at most
70% of the viewport), so long content scrolls inside the editor and the toolbar stays
visible; a height on the field definition wins. Document editables always grow with their
content, and a template height (pimcore_wysiwyg('intro', { height: 300 })) is
ignored: a capped field inside the editmode iframe would catch the mouse wheel and stop
the page from scrolling while the pointer is over its text. Unknown
items are skipped by CKEditor with a console warning, nothing breaks. Adding an item
that needs a new plugin means adding the plugin in
assets/studio/js/src/modules/ckeditor-editor/editor.tsx and rebuilding. The AI
gate appends its own toolbar buttons (toggleAi, aiQuickActions); other AI items
(ask-ai, improve-writing) can be added here once the gate is on.
Per-field tweaks remain possible: whatever a field definition or
pimcore_studio_ui.wysiwyg.default_editor_config passes as editorConfig is spread
over the bundle config last, so genuine CKEditor options set there win (note that
Pimcore 2026.x writes that config Quill-shaped; CKEditor ignores unknown keys).
Inline editor for document editables
Document editables use the classic editor (toolbar above a framed editing area) by default. A template can switch a single editable to CKEditor's inline editor, which edits the text in place in the page layout and shows a floating toolbar while the field is focused:
{{ pimcore_wysiwyg('heroSubline', { editorType: 'inline', placeholder: 'One or two sentences' }) }}
The text keeps the page's own typography; a light frame marks the field on hover
and while focused. The floating toolbar is not grouped into an overflow menu, so
narrow columns still show every button. fullscreen is left out of the inline
toolbar (CKEditor's Fullscreen supports the classic and decoupled editor types only).
CKEditor AI, the shared AI Context and real-time collaboration work the same way.
The option applies to document editables only; data object fields always use the
classic editor. The landing page document type of the guidepost demo repository
(templates/landing/landing.html.twig) uses it for all its rich text fields.
Premium feature gates
Two premium features are wired behind config gates: CKEditor AI and real-time
collaboration. The premium code (ckeditor5-premium-features, pinned to the exact
same version as ckeditor5) is loaded by the browser through a dynamic import ONLY
when both a license key is configured and at least one gate is enabled. With no key,
or with the gates off, the editor initializes exactly as the plain open source build,
no premium chunk is even fetched (the premium code and CSS live in their own async
chunks), and Studio's Content Security Policy stays untouched. A gate whose
prerequisites are missing logs a console warning and keeps the free feature set, so
a misconfigured instance never breaks. The wiring lives in
assets/studio/js/src/modules/ckeditor-editor/premium.ts.
This repository commits config/packages/ckeditor_studio.yaml with both gates
driven by environment variables that default to off:
parameters: env(CKEDITOR_PREMIUM_AI): '0' env(CKEDITOR_PREMIUM_RTC): '0' ckeditor_studio: premium: ai: enabled: '%env(bool:CKEDITOR_PREMIUM_AI)%' # Where the AI UI renders: sidebar (default, one docked panel per element # next to the content) or overlay (floats over the page). # container: sidebar # Optional raw CKEditor "ai" configuration, passed to the shared Context # (serviceUrl, chat, ...). # config: {} realtime_collaboration: enabled: '%env(bool:CKEDITOR_PREMIUM_RTC)%' # Leave both null to use the bundle's own token endpoint and # CKEDITOR_CS_WEBSOCKET_URL; set them to point at your own services. # token_url: ~ # web_socket_url: ~
The full set of environment variables the bundle reads (all optional, all empty by
default; set them in .env.local or your secret store, never in committed files):
| Variable | Used for |
|---|---|
CKEDITOR_LICENSE_KEY |
licenseKey in the browser; empty means 'GPL' |
CKEDITOR_CS_ENVIRONMENT_ID |
Cloud Services environment id, the token aud claim (server side) |
CKEDITOR_CS_ACCESS_KEY |
Cloud Services access key, signs the tokens (server side only, never sent to the browser) |
CKEDITOR_CS_WEBSOCKET_URL |
Cloud Services WebSocket URL for real-time collaboration |
CKEDITOR_PREMIUM_AI |
gate: CKEditor AI (1 / 0, via the yaml above) |
CKEDITOR_PREMIUM_RTC |
gate: real-time collaboration (1 / 0, via the yaml above) |
What turning each gate on does (with a license key configured):
premium.ai.enabledturns on CKEditor AI with ONE chat per Pimcore element that sees every wysiwyg field of that element (see "One AI chat per element" below), and appends thetoggleAiandaiQuickActionsbuttons to the toolbar (they cannot sit in the yaml toolbar, because the free path has no plugin behind them). The AI service authenticates with the Cloud Services token, so it needs the token endpoint even when the collaboration gate is off.premium.realtime_collaboration.enabledloadsRealTimeCollaborativeEditing(remote carets and selections included) plus a presence list rendered above the editor, withcloudServicesbuilt from the token endpoint and the WebSocket URL. It also setssourceEditing.allowCollaborationFeatures: true, because the free SourceEditing plugin otherwise refuses to start next to real-time collaboration. With collaboration active, the document state lives in Cloud Services and the editor stops applying external value resets; changes still flow back into the Pimcore form (the object shows as modified and saves normally).
Collaboration channel id: derived per field as
pimcore-<elementType>-<elementId>-<fieldName>, for example
pimcore-data-object-3-content or pimcore-document-1-intro. Data object fields take
the element from Studio's element context and the field from combinedFieldName
(nested fields such as localized fields stay unique). Document editables render in
the editmode iframe, where Studio passes neither: the element id comes from the
iframe page (window.pimcore_document_id) and the field from the editable's
#pimcore_editable_<name> container. An explicit per-field
editorConfig.collaboration.channelId always wins.
One AI chat per element (shared Context)
With the AI gate on, all editors of one Pimcore element share one CKEditor
Context (CKEditor's multi-editor AI support, stable since v48.3):
- The Context (key and channel
pimcore-<elementType>-<elementId>) carries the AI feature plugins (AIChat,AIChatHistory,AIChatShortcuts,AIReviewMode,AITranslate),CloudServices, theaiconfiguration (includingpremium.ai.config) and, with the RTC gate on too, one sharedPresenceList. - Each editor keeps
AIEditorIntegration+AIQuickActions(+ RTC) and describes itself throughroot.title/root.description(field title, or the humanized editable name such as "Found resources"), which is how the AI tells fields apart and puts an edit in the right one. - The Context is created by the first editor of the element, reference counted, and destroyed shortly after the last one unmounts (element tab closed). The free path and the AI-off path create no Context.
AI panel (premium.ai.container): sidebar (default) renders the chat into one
panel per element, docked on the right of the scroll container that holds the
element's editors (the editmode iframe for documents, Studio's edit panel for data
objects); that container gets matching right padding, so the content moves aside
instead of being covered. The panel is 32% of the available width, clamped to 300
to 440px, hides with the AI toggle button, follows Studio's tab switching, and the
editor's fullscreen mode takes the chat along and hands it back on exit. overlay
restores CKEditor's floating panel. Studio has a sidebar extension point for
documents only (Document/Editor/Sidebar/*SidebarManager) and none for data
objects, and document editables live in an iframe while that sidebar lives in the
parent window, so the bundle docks its own panel instead.
Cloud Services token endpoint
The bundle ships its own token endpoint:
GET /pimcore-studio/api/ckeditor-studio/cs-token (route ckeditor_studio_cs_token).
- Lives under
/pimcore-studio/api, so the Studio firewall and itsROLE_PIMCORE_USERaccess rule apply: anonymous requests get401, and the editor fetches the token with the Studio session cookie (same origin). - Returns a plain-text HS256 JWT signed with
CKEDITOR_CS_ACCESS_KEY:aud=CKEDITOR_CS_ENVIRONMENT_ID,sub= the Pimcore user id,user.name(first and last name, falling back to the username) anduser.email,iat/exp(1 hour),auth.collaboration={"*": {"role": "writer"}}(demo scope: every channel), andauth.ai.permissions= the "end user" profile from the CKEditor AI docs (ai:conversations:read,ai:conversations:write,ai:actions:*,ai:reviews:*,ai:models:agent). Narrow both for production. - Returns
503when the Cloud Services variables are not set. When they are set andpremium.realtime_collaboration.token_urlis null, the editor'stokenUrldefaults to this route automatically. - The access key never leaves the server; only the token URL reaches the browser.
Studio's CSP: Pimcore Studio sends a strict connect-src, which blocks the browser's
calls to Cloud Services (AI over HTTPS, collaboration over a WebSocket) and the
license usage endpoint. A bundle CspEvent subscriber adds https://*.cke-cs.com,
wss://*.cke-cs.com, the cke-cs-eu.com equivalents and
https://proxy-event.ckeditor.com to connect-src, only while a license key is set
and a gate is on.
Testing premium locally
A free trial from portal.ckeditor.com (no credit card) provides everything the gates need: a trial license key covering the premium features, and a Cloud Services environment (environment id, an access key you create in the portal, the WebSocket URL). Keys are channel-scoped; this bundle self-hosts via npm, so use a key valid for self-hosted use. Then:
-
Put the values in
.env.local(gitignored, never commit them):CKEDITOR_LICENSE_KEY=<license key> CKEDITOR_CS_ENVIRONMENT_ID=<environment id> CKEDITOR_CS_ACCESS_KEY=<access key> CKEDITOR_CS_WEBSOCKET_URL=<wss://...cke-cs.com/ws> CKEDITOR_PREMIUM_AI=1 CKEDITOR_PREMIUM_RTC=1
-
docker compose exec php bin/console cache:clear. No frontend rebuild and no yaml change: the gates and URLs are runtime configuration read from the Studio shell page. -
Open a data object with a wysiwyg field in Studio (in this repo:
/blog/hello-ckeditor, field Content).
What to expect (verified live on 2026-11-05 with a long-term trial, Chrome):
- The console shows CKEditor's trial license notice and no license error.
- The premium chunks load only now; the token endpoint returns
200for the logged-in user. - The AI panel is docked next to the content (not over it); the toolbar carries the AI and AI Quick Actions buttons. A chat message such as "Summarize this document in one sentence." returns an answer about the current document.
- The presence list above the toolbar shows the current user (the Pimcore user from
the token's
sub), the channel id ispimcore-data-object-3-content, and an edit made in a second tab on the same object appears live in the first one. Two tabs of the SAME Pimcore user show one avatar; log in as a second Pimcore user to see two. - On a document with several editables (the Home page has five), the chat lists all of them by title and an edit request that names one field ("add a sentence to the Intro that mentions the Reference points") lands in that field only.
- Set both gates back to
0(or remove the key) andcache:clear: no premium chunk loads and Studio's CSP is back to its originalconnect-src.
Backend prerequisites beyond the key, with pointers:
- CKEditor AI needs the AI service on your Cloud Services environment (trial
environments include it) and a token with
auth.ai.permissions; see the CKEditor AI integration guide, CKEditor AI authentication and permissions. The default AI service URL is the US region; for an EU environment setpremium.ai.config.serviceUrltohttps://ai.cke-cs-eu.com/v1. - Real-time collaboration needs the Cloud Services environment and a token with
auth.collaboration; see the token endpoint guide and the collaboration quick start.
Limitations
- Editor selection relies on plugin priorities (this bundle at -1000 and 1000). An editor bundle that also uses an extreme priority could still race it; Studio itself has no slot arbitration (see issue #2 for the upstream angle). Per-class or per-field editor choice is not implemented.
- Document editables are identified from the editmode iframe DOM
(
#pimcore_editable_<name>,window.pimcore_document_id) because Studio core passes neither; a Pimcore change to that markup would need a matching change here. - The docked AI panel was checked live on a wide screen and at laptop size
(1384x797, panel 300px). In document editmode it is fixed inside the editmode
iframe and narrows the page content without changing the viewport width, so a
template that switches layout with viewport media queries keeps its wide layout and
overflows under the panel. Prefer layouts that adapt to the available width
(
repeat(auto-fit, minmax(...)), flex wrap, container queries). - The token endpoint grants every Studio user writer access to every channel and the AI end-user profile. Fine for a demo; production should map Pimcore permissions to per-channel roles.
- Pimcore's per-field
editorConfigis Quill-shaped in 2026.x; no mapping to CKEditor options is implemented (pass-through only). - Dropping an element onto selected text applies only
href, not thepimcore_id/pimcore_typeattributes. - No translations shipped; no automated tests.
Layout
composer.json PHP package (type: pimcore-bundle, PSR-4 Simply007\CKEditorStudioBundle)
src/ Bundle class, configuration, app config provider, entry point provider,
Cloud Services token generator + controller, premium CSP subscriber
config/studio_ui.yaml Tags the providers for Studio UI (incl. document editor iframe)
config/pimcore/routing.yaml Token endpoint route (auto-loaded by Pimcore's bundle routing loader)
assets/studio/ TypeScript frontend (rsbuild + module federation, port 3033)
public/studio/build/<id>/ Committed frontend build (entrypoints.json consumed by PHP)
Development and releases
- Frontend build: committed under
public/studio/build/<build-id>/, see "Why the repository contains a compiled frontend" below. Maintainers rebuild withnpm ci && npm run buildinassets/studio/and commit the output together with the source change. - Studio registration: nothing manual.
extra.pimcore.bundlesmakespimcore:bundle:enable CKEditorStudioBundlework, the prependedstudio_ui.yamlregisters the entry point and app config providers only whenpimcore_studio_uiis present, andassets:installpublishes the build. - Versioning: pin
pimcore/studio-ui-bundle(composer) and@pimcore/studio-ui-bundle(npm) to the same minor line; the frontend consumes Studio's SDK types at build time and its module federation remote at runtime. - Releases are git tags (
v0.1.0first). CI runsphp -l, the TypeScript check and the production build on every push. TESTING.mdis the runtime test plan written against the guidepost demo stack;SKILL-FINDINGS.mdrecords what the agent skills used to build the bundle got right and wrong.
Why the repository contains a compiled frontend
Most PHP packages contain only source code. This one also commits a compiled
JavaScript build in public/studio/build/<build-id>/, and pull requests that touch the
frontend show large minified diffs there. That is on purpose:
- Studio runs compiled code. Pimcore Studio is a React app that loads each bundle's
frontend at runtime as a module federation remote. It reads the bundle's
entrypoints.jsonand JavaScript frompublic/bundles/<bundle>/studio/build/<build-id>/(published byassets:install). It never compiles the TypeScript inassets/studio/. - Composer does not build anything. Composer installs the repository exactly as it is at the release tag (Packagist serves the tag's archive) and runs no Node step. Pimcore projects usually have no Node toolchain either. Whatever is committed at the tag is what Studio gets.
- The build id ties build and source together. The folder name is a fingerprint of the frontend source, so it changes exactly when the source changes. CI rebuilds on every push and fails when the committed build id does not match the source.
Alternatives, if committed build files become a burden:
| Approach | How it works | Trade-off |
|---|---|---|
| Commit the build (current) | Rebuild and commit public/studio/build/ with every frontend change |
Simple, nothing extra at install or runtime; noisy diffs |
| Build on release tags | CI builds when a v* tag is pushed and commits the output only to the release tag or a release branch; main stays source-only |
Clean main, same install experience; the release is a CI job instead of a local step |
| Release archive | Ship the build as a zip that the PHP side extracts on demand through Studio's BuildArchiveProviderInterface, the way pimcore/quill-bundle does |
Fewer files in the package; needs a PHP change and a CI job that produces the zip |
Licensing
In short: this bundle is open source (GPL-2.0-or-later). The CKEditor premium features it can switch on are commercial and need a CKEditor license key. Pimcore has its own license. This section is a summary to help you decide, not legal advice.
| Part | License | Where it is |
|---|---|---|
| This bundle's own code (PHP, TypeScript) | GPL-2.0-or-later, see LICENSE |
src/, config/, assets/ |
CKEditor open source (ckeditor5) |
Dual: GPL-2.0-or-later, or commercial terms from CKSource | Bundled in the committed build |
CKEditor premium features (ckeditor5-premium-features: CKEditor AI, real-time collaboration and others) |
Commercial license from CKSource, not covered by the GPL | Bundled in the committed build, loaded only when a license key is configured and a premium gate is on |
| Other libraries in the build (React and helpers) | MIT, Apache-2.0, BSD, ISC | Notices in the *.LICENSE.txt files next to the build output |
| Pimcore and Pimcore Studio | Pimcore Open Core License (POCL), or commercial terms from Pimcore | Not included in this package: the bundle uses Studio's SDK at runtime |
What this means in practice:
- Without a license key the editor runs with
licenseKey: 'GPL'and only open source CKEditor code is executed. - Running Pimcore with this bundle on your own servers is use, not distribution, so the GPL's distribution terms do not come into play.
- Distributing a project that combines this bundle with Pimcore (for example, an agency handing a project to a client) is where licenses meet: the GPL covers the bundle and CKEditor, the POCL covers Pimcore, and the POCL is not an open source license. Check that the combination works for your case. A CKEditor commercial license lets you use CKEditor under commercial terms instead of the GPL.
- Premium features (CKEditor AI, real-time collaboration, and others) always need a CKEditor commercial or trial license key: https://portal.ckeditor.com.
More: CKEditor licensing options, CKEditor pricing, Pimcore licensing.
