Skip to content

rich_text — Rich text ​

WYSIWYG rich text, on TinyMCE or CKEditor.

The rich_text block, rendered

A full WYSIWYG editor for formatted copy: bold/italic, links, lists, headings, colors and images. Which editor mounts it is the host's choice — TinyMCE (default) or CKEditor 5 — set once in configuration. Whichever runs, the block stores the same { content: "<html>" }, so switching editors is a config change and never a data migration. The color swatches are wired to the project palette (cb_color_palette()), so rich text stays on-brand under either.

When to use ​

Long-form content, marketing copy, anything needing inline formatting or links.

Configuration ​

Configure under content_blocks_kit.blocks.rich_text (see Configuring blocks for how the levers combine).

Options ​

Block-level knobs, set under options:.

OptionDefault
editortinymce
cdntrue
script_urlnull
style_urlnull
cdn_urlnull
cdn_style_urlnull
uploadstrue
config[]

Default data ​

Initial values for a new block. Override per host with defaults:.

FieldDefault
content''

Example ​

yaml
# config/packages/content_blocks_kit.yaml
content_blocks_kit:
    blocks:
        rich_text:
            options: { editor: tinymce }
            defaults: { content: '' }

Front-end ​

Rendered markup: .cb-kit-rich-text (the stored HTML is rendered as-is). Style it by overriding the --cb-kit-* custom properties (see the kit stylesheet).

Requires a Stimulus controller

Enable cb-tinymce / cb-ckeditor under @klehm/content-blocks-kit in your host assets/controllers.json (whichever you select — both, if you are unsure).

Choosing an editor ​

yaml
# config/packages/content_blocks_kit.yaml
content_blocks_kit:
    blocks:
        rich_text:
            options:
                editor: ckeditor      # tinymce (default) | ckeditor

Both editors are wired the same way, so everything below applies to either.

Where the editor is loaded from ​

By default the kit loads the editor from a CDN, so the block works on a fresh install with nothing to build. Two other paths are supported, and neither adds a dependency to the kit:

yaml
content_blocks_kit:
    blocks:
        rich_text:
            options:
                # 1. Self-host the same build — for a strict CSP, or an admin
                #    with no outbound internet access. `asset:` runs the path
                #    through your asset packages, which is what a versioned
                #    filename needs (see below).
                script_url: 'asset:vendor/ckeditor5.umd.js'
                style_url: 'asset:vendor/ckeditor5.css'   # CKEditor only

                # 2. Bundle it yourself: the kit then loads nothing and expects
                #    the editor's global (window.tinymce / window.CKEDITOR) to
                #    be there when the sidebar opens.
                cdn: false

cdn_url and cdn_style_url are the names these two replaced and still work. They read as "another CDN", which is not what self-hosting is.

With cdn: false it is on the host to npm install the editor and import it in the entry that serves the admin — for example import 'tinymce/tinymce.min.js' (or CKEditor's UMD build), so that the global is defined. If it is missing, the block logs a precise message to the console and leaves a plain textarea holding the HTML: the content is always editable and never lost, whatever happens to the editor.

Pinned versions

The default CDN URLs point at TinyMCE 7 and CKEditor 5.48. CKEditor's editor factory changed in 48; the controller reads window.CKEDITOR_VERSION and calls the older signature for an older self-hosted build.

Images ​

Image upload is wired by default and goes through the builder's own endpoint — the same /_content-blocks/upload, CSRF token, size cap and MIME allow-list the image block uses, so a picture pasted into rich text lands in the same storage as every other asset. It follows that uploads need file storage configured; without it the endpoint refuses and the editor reports the error.

Turn it off with options: { uploads: false } — the image button disappears and pasted images are no longer sent anywhere.

Configuring the editor itself ​

options.config is merged over the kit's init config in the browser, so anything the editor accepts can be set without forking the block:

yaml
content_blocks_kit:
    blocks:
        rich_text:
            options:
                config:
                    height: 500
                    toolbar: 'bold italic | link'    # TinyMCE spelling

Objects merge key by key; arrays and scalars replace. A toolbar you spell out is a replacement, never an append.

Two things the merge deliberately protects, because losing them silently would cost data rather than looks: the write-back that keeps autosave in sync (a setup callback of your own runs after it rather than replacing it), and the upload adapter, which survives a replaced plugin list — use uploads: false if you mean to drop it.

Naming a versioned asset ​

A digested filename — /assets/styles/wysiwyg-8f3a2c.css — cannot be written by hand in a static YAML file, and asset() only exists in Twig. Prefix the path with asset: and the kit resolves it through your asset packages (AssetMapper, Encore, any package you configured), at any depth in the config:

yaml
options:
    config:
        # The usual case: the editing surface should look like the page.
        content_css: 'asset:styles/wysiwyg.css'

The list form works too (['asset:a.css', 'https://cdn.example/b.css']), and a value without the prefix is left exactly as written. If no asset packages are configured, an asset: value raises an explicit error rather than emitting a URL that would 404 inside the editor.

Needs symfony/asset

Resolution goes through assets.packages, so symfony/asset must be installed and framework.assets enabled. AssetMapper does not pull it in on its own — an app that only ever uses importmap() can be missing it and not know.

Anything JSON cannot carry ​

Callbacks, custom buttons, plugin instances, a stylesheet list only your bundler knows: those never travel through YAML. Listen for cb-rich-text:configure instead — it fires on the field wrapper, bubbling, once the config is merged and before the editor is created. detail.config is the live object:

js
// your admin entry — no controller to fork, no adapter to write
document.addEventListener('cb-rich-text:configure', (event) => {
    if (event.detail.editor !== 'tinymce') return;

    event.detail.config.content_css = window.MY_EDITOR_CSS;   // hashes included
    event.detail.config.setup = (editor) => {
        editor.ui.registry.addButton('twocolumns', { text: '2 Cols', onAction: () => { /* … */ } });
    };
});

The two protections above still hold after the event: your setup runs after the autosave write-back, and the upload adapter is appended after your plugin list.

Adding a third editor ​

Neither editor is special. A host registers Quill, Trix or an in-house editor by implementing ContentBlocks\Kit\RichText\RichTextEditorInterface — extending AbstractRichTextEditor gives the shared option handling for free — and shipping a Stimulus controller with the name it returns. The service is auto-tagged, so it becomes selectable as options.editor: <name> with no further wiring, and nothing in the kit changes.

Notes ​

  • The editor JavaScript lives in the sidebar, never in the preview — so the block opts into preview hot-reload.
  • Color swatches read content_blocks.palette via cb_color_palette(), under both editors.
  • The stored HTML is rendered with |raw: like html_raw, the block trusts whoever may edit it. Unlike html_raw it stays enabled by default, because a toolbar — not a free HTML field — is what produces the markup.

The configuration tables above are generated from content-blocks-kit:blocks --format=json, read straight from the block's code — they never go stale.

Released under the MIT License.