Sign up (with export icon)

Editor bundling with Vite

Show the table of contents

This guide shows how to build a CKEditor 5 editor bundle with Vite and upload it to CKEditor Cloud Services. The guide uses React as an example, but the same logic applies to other frameworks.

Note

If you use webpack, refer to the Editor bundling with webpack guide.

How the setup works

Copy link

In a framework application, a component usually defines the editor configuration together with DOM elements and callbacks that exist only in the browser. The bundle cannot use such a component. This guide moves the editor definition into plain modules that the application and the bundle share:

  • A shared editor module defines the editor class with its plugins, and the part of the configuration that Cloud Services needs.
  • The application adds the runtime configuration: credentials, the document ID, DOM elements, functions, and classes. It also imports the editor styles.
  • A second Vite configuration builds the bundle from a small entry file.
  • An upload script sends the bundle, the shared configuration, and the test data to Cloud Services.

The project has this structure:

├── src
│   ├── editor
│   │   ├── editor.js           # Shared: the editor class and its plugins.
│   │   ├── config.js           # Shared: the configuration and the bundle version.
│   │   ├── bundle-entry.js     # Bundle only: the entry file of the editor bundle.
│   │   └── plugins/            # Your custom plugins.
│   ├── App.jsx                 # Application only: the runtime configuration.
│   └── main.jsx                # Application only: the editor styles.
├── scripts
│   └── upload-bundle.js        # Uploads the editor bundle.
├── test-data
│   └── document.html           # The test data for the upload.
├── .gitignore                  # Also ignores dist-bundle/ and .env.upload.local.
├── package.json
├── vite.config.js              # The application build.
└── vite.bundle.config.js       # The editor bundle build.
Copy code

Dependencies

Copy link

This example uses the following dependencies:

  • ckeditor5 and ckeditor5-premium-features – The editor and the premium features, for example real-time collaboration.
  • @ckeditor/ckeditor5-react – The React integration. In other frameworks, use their integration instead.
  • react and react-dom – The application framework.
  • vite and @vitejs/plugin-react – The build tool of the application. The bundle build needs only Vite, without other plugins or loaders.

If your Vite application does not have the CKEditor 5 packages yet, add them:

npm install ckeditor5 ckeditor5-premium-features @ckeditor/ckeditor5-react
Copy code

After that, your package.json file should contain these packages:

{
	// ...
	"dependencies": {
		"@ckeditor/ckeditor5-react": "^12.0.0",
		"ckeditor5": "^49.0.0",
		"ckeditor5-premium-features": "^49.0.0",
		"react": "^19.2.8",
		"react-dom": "^19.2.8"
	},
	"devDependencies": {
		// ...
		"@vitejs/plugin-react": "^6.1.1",
		"vite": "^8.3.0"
	}
}
Copy code

Shared editor module

Copy link

Editor class

Copy link

The editor.js file defines the AppEditor class and lists all its plugins in builtinPlugins. Cloud Services creates the editor from this class, so the plugins must be in the bundle. You cannot add them with the config.plugins option. The name AppEditor keeps the class apart from the Editor base class of ckeditor5.

// src/editor/editor.js

import {
	ClassicEditor,
	Autosave,
	BlockQuote,
	Bold,
	Essentials,
	GeneralHtmlSupport,
	Heading,
	Image,
	ImageCaption,
	ImageInsertViaUrl,
	ImageToolbar,
	Italic,
	Link,
	List,
	Paragraph,
	Table,
	TableToolbar,
	Underline,
	WordCount
} from 'ckeditor5';

import {
	Comments,
	PresenceList,
	RealTimeCollaborativeComments,
	RealTimeCollaborativeEditing,
	RealTimeCollaborativeRevisionHistory,
	RealTimeCollaborativeTrackChanges,
	RevisionHistory,
	TrackChanges
} from 'ckeditor5-premium-features';

import { PlaceholderEditing } from './plugins/placeholder/placeholderediting.js';
import { PlaceholderUI } from './plugins/placeholder/placeholderui.js';

// The editor class that the application and the editor bundle share.
export class AppEditor extends ClassicEditor {}

AppEditor.builtinPlugins = [
	Autosave,
	BlockQuote,
	Bold,
	Essentials,
	GeneralHtmlSupport,
	Heading,
	Image,
	ImageCaption,
	ImageInsertViaUrl,
	ImageToolbar,
	Italic,
	Link,
	List,
	Paragraph,
	Table,
	TableToolbar,
	Underline,
	WordCount,

	Comments,
	PresenceList,
	RealTimeCollaborativeComments,
	RealTimeCollaborativeEditing,
	RealTimeCollaborativeRevisionHistory,
	RealTimeCollaborativeTrackChanges,
	RevisionHistory,
	TrackChanges,

	// List the parts of a custom feature, not only its glue plugin. The upload removes `PlaceholderUI`,
	// and a glue plugin that requires it would stop the upload with the `plugincollection-required` error.
	PlaceholderEditing,
	PlaceholderUI
];
Copy code

PlaceholderEditing and PlaceholderUI are the two parts of a custom feature, based on the inline widget tutorial. The Custom plugins section explains why the bundle needs the first one and Cloud Services removes the second one.

Shared configuration

Copy link

The config.js file has the part of the editor configuration that the application and Cloud Services share:

// src/editor/config.js

// The part of the editor configuration that the application and Cloud Services share.
// The upload script sends it as JSON, so it must not contain DOM elements, functions or classes.
// Regular expressions in `htmlSupport` are allowed: the upload script converts them to strings.

// Change this value every time you change the editor bundle or this configuration.
export const BUNDLE_VERSION = 'react-vite-1.0.0';

export const editorConfig = {
	cloudServices: {
		bundleVersion: BUNDLE_VERSION
	},
	toolbar: {
		items: [
			'undo', 'redo',
			'|', 'revisionHistory', 'trackChanges', 'comment',
			'|', 'heading',
			'|', 'bold', 'italic', 'underline',
			'|', 'link', 'insertImage', 'insertTable', 'blockQuote',
			'|', 'bulletedList', 'numberedList',
			'|', 'placeholder'
		]
	},
	image: {
		toolbar: [ 'toggleImageCaption', 'imageTextAlternative', '|', 'comment' ]
	},
	table: {
		contentToolbar: [ 'tableColumn', 'tableRow', 'mergeTableCells' ]
	},
	htmlSupport: {
		allow: [
			{
				name: /^(div|section)$/,
				classes: true
			}
		]
	},
	placeholderConfig: {
		typesUrl: '/placeholder-types.json'
	}
};

// Plugins that only the browser needs. The upload script lists them in `removePlugins`,
// so Cloud Services removes them before it uses the bundle.
export const pluginsRemovedOnServer = [ 'PlaceholderUI' ];
Copy code

Keep these rules for the shared configuration:

  • The upload script sends the configuration as JSON. DOM elements, functions, and classes do not survive JSON.stringify(), so they go in the runtime configuration of the application. The upload script converts the regular expressions of the General HTML Support configuration to strings, because Cloud Services expects them in this format.
  • The upload script imports this module in Node.js. Thus the module must not import ckeditor5 or other browser code. Node.js cannot load ckeditor5 and stops with TypeError: domDocument.createElement is not a function.
  • BUNDLE_VERSION is the only place for the bundle version. The application and the upload script read it from this module, so they always use the same value.
  • pluginsRemovedOnServer lists the plugins that Cloud Services must not run.

Using the shared module in the application

Copy link

The application imports the editor class and the shared configuration, and adds the runtime configuration. In React, give both to the <CKEditor> component of the React integration:

// src/App.jsx

import { useState } from 'react';
import { CKEditor } from '@ckeditor/ckeditor5-react';
import { Bold, Italic, List } from 'ckeditor5';

import { AppEditor } from './editor/editor.js';
import { editorConfig } from './editor/config.js';

// ...

// The URL hash holds the document ID, so that two windows with the same URL share one session.
const documentId = getDocumentId();

export default function App() {
	// React gives the layout element to `setLayout` after it renders the containers of the editor UI.
	const [ layout, setLayout ] = useState( null );

	return (
		<div className="main-container" ref={ setLayout }>
			{ /* ... */ }
			{ layout && <CKEditor editor={ AppEditor } config={ getRuntimeConfig( layout ) } /> }
			{ /* ... */ }
		</div>
	);
}

// The shared configuration plus the values that exist only in the browser:
// credentials, the document ID, DOM elements, functions and classes.
function getRuntimeConfig( layout ) {
	const element = selector => layout.querySelector( selector );

	return {
		...editorConfig,
		licenseKey: import.meta.env.VITE_CKEDITOR_LICENSE_KEY,
		cloudServices: {
			...editorConfig.cloudServices,
			tokenUrl: import.meta.env.VITE_CKEDITOR_TOKEN_URL,
			webSocketUrl: import.meta.env.VITE_CKEDITOR_WEBSOCKET_URL
		},
		collaboration: {
			channelId: documentId
		},
		// ...
		sidebar: {
			container: element( '.editor-container__sidebar' )
		},
		// ...
		comments: {
			editorConfig: {
				extraPlugins: [ Bold, Italic, List ]
			}
		},
		autosave: {
			save: editor => saveDocument( editor.getData() )
		}
	};
}

// ...
Copy code

The editor renders only after React gives the layout element to setLayout, so the containers of the editor UI exist at that moment. The component reads no refs during render, so the React rules of the Vite template report no warnings. The configuration does not contain the plugins option, so the editor uses builtinPlugins of the shared class.

Vite gives the browser only the environment variables with the VITE_ prefix. Put the license key and the Cloud Services URLs in .env.local, but never the API secret.

Import the editor styles in the application, for example in main.jsx. Keep them out of the shared modules, because the bundle does not need them:

// src/main.jsx

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

import 'ckeditor5/ckeditor5.css';
import 'ckeditor5-premium-features/ckeditor5-premium-features.css';

import App from './App.jsx';

createRoot( document.getElementById( 'root' ) ).render(
	<StrictMode>
		<App />
	</StrictMode>
);
Copy code

In other frameworks, give the same class and configuration to the integration component. In plain JavaScript, give them to AppEditor.create():

// src/plain-javascript.js

import { Bold, Italic, List } from 'ckeditor5';

import { AppEditor } from './editor/editor.js';
import { editorConfig } from './editor/config.js';

import 'ckeditor5/ckeditor5.css';
import 'ckeditor5-premium-features/ckeditor5-premium-features.css';
// ...

// The same runtime configuration as in the React application, with the elements of this page.
AppEditor.create( {
	...editorConfig,
	attachTo: document.querySelector( '#editor' ),
	licenseKey: import.meta.env.VITE_CKEDITOR_LICENSE_KEY,
	cloudServices: {
		...editorConfig.cloudServices,
		tokenUrl: import.meta.env.VITE_CKEDITOR_TOKEN_URL,
		webSocketUrl: import.meta.env.VITE_CKEDITOR_WEBSOCKET_URL
	},
	collaboration: {
		channelId: window.location.hash.slice( 1 )
	},
	// ...
	sidebar: {
		container: document.querySelector( '#sidebar' )
	},
	// ...
} );
Copy code

Bundle entry file

Copy link

The bundle has its own entry file, so that it contains only the editor:

// src/editor/bundle-entry.js

// The entry file of the editor bundle. Cloud Services uses its default export as the `CKEditorCS` global.
import { AppEditor } from './editor.js';

export default AppEditor;
Copy code

The default export must be the only export. In Vite library mode, an entry file with named exports makes the CKEditorCS global an object with the class in its default property. Then the upload fails. The Vite configuration below stops such a build.

Vite configuration for the bundle

Copy link

A second Vite configuration builds the bundle in library mode:

// vite.bundle.config.js

import { defineConfig } from 'vite';

// Builds the editor bundle for Cloud Services: one file that sets the `CKEditorCS` global.
export default defineConfig( {
	// Resolve the paths below from this folder, not from the folder that runs the build.
	root: import.meta.dirname,

	// Vite does not replace `process.env` in library mode, and the Cloud Services page has no `process` global.
	define: {
		'process.env.NODE_ENV': JSON.stringify( 'production' )
	},

	build: {
		outDir: 'dist-bundle',
		emptyOutDir: true,
		copyPublicDir: false,
		lib: {
			entry: 'src/editor/bundle-entry.js',
			name: 'CKEditorCS',
			formats: [ 'umd' ],
			fileName: () => 'editor.bundle.js'
		},
		rolldownOptions: {
			output: {
				// Stop the build if the entry file exports anything other than the editor class.
				exports: 'default'
			}
		}
	}
} );
Copy code

The configuration uses these options:

  • root – Vite resolves lib.entry and outDir from the folder of this file. This matters when you start the build from another folder, for example from the root of a monorepo.
  • define – Replaces process.env.NODE_ENV. Cloud Services runs the bundle in a page that has no process global. Without this option, a dependency that reads process.env, for example React, stops the bundle.
  • build.lib – Builds one UMD file that sets the CKEditorCS global to the default export of the entry file. Cloud Services cannot load an ES module, so do not use the es format.
  • build.rolldownOptions.output.exports – Stops the build if the entry file has other exports than the default export.
  • outDir and copyPublicDir – Keep the bundle apart from the application build in dist/.

Vite puts the code of dynamic imports into the UMD file, so you do not need an equivalent of the dynamicImportMode: 'eager' webpack option. If your plugins import CSS files, Vite also writes a CSS file to dist-bundle/. The upload does not need it.

Test data

Copy link

Cloud Services checks the uploaded bundle with the test_data parameter. It creates the editor with this data and compares the editor output with test_data character by character. The editor changes hand-written HTML. For example, it adds a data-list-item-id attribute to each list item. It also writes some spaces next to inline widgets as &nbsp;, and it changes <h1> to <h2>. Thus, take the test data from the editor itself:

  1. In your application, open a document that uses all features and custom plugins of your editor.

  2. In the developer tools of the browser, copy the editor data with this command:

    copy( document.querySelector( '.ck-editor__editable' ).ckeditorInstance.getData( { trim: 'none' } ) );
    Copy code

    The command gets the editor instance from the editable element with the ckeditorInstance property. CKEditor 5 has no global list of editor instances.

  3. Compare the data with the original document, as the Making the test data section describes. The upload does not find the content that the editor loses when it first loads the document.

  4. Paste the data into test-data/document.html. Do not format it.

Make new test data each time you change the plugins or update CKEditor 5. For a checklist of the content to test, the changes that the editor makes, and a local check, refer to the Testing the editor bundle with test data guide.

Upload script

Copy link

The upload script sends the bundle with the POST /editors request:

// scripts/upload-bundle.js

// Uploads the editor bundle to Cloud Services with the `POST /editors` request.
// It reads the credentials from environment variables. Never put them in the frontend code.

import { createHmac } from 'node:crypto';
import { readFile } from 'node:fs/promises';

import { editorConfig, pluginsRemovedOnServer } from '../src/editor/config.js';

const { CKEDITOR_CS_API_BASE_URL: apiBaseUrl, CKEDITOR_CS_API_SECRET: apiSecret } = process.env;

if ( !apiBaseUrl || !apiSecret ) {
	console.error( 'Set the CKEDITOR_CS_API_BASE_URL and CKEDITOR_CS_API_SECRET environment variables.' );
	process.exit( 1 );
}

const bundle = await readFile( new URL( '../dist-bundle/editor.bundle.js', import.meta.url ), 'utf8' );

// Cloud Services compares the test data with the editor output exactly, so remove the final new line of the file.
const testData = ( await readFile( new URL( '../test-data/document.html', import.meta.url ), 'utf8' ) ).trimEnd();

const config = {
	...editorConfig,
	removePlugins: pluginsRemovedOnServer
};

// Cloud Services expects the regular expressions of `htmlSupport` as strings, for example "/^(div|section)$/".
const body = JSON.stringify( { bundle, config, test_data: testData }, ( _key, value ) => {
	return value instanceof RegExp ? value.toString() : value;
} );

const url = new URL( `${ apiBaseUrl }/editors` );
const timestamp = Date.now();
const signature = createHmac( 'sha256', apiSecret )
	.update( `POST${ url.pathname }${ url.search }${ timestamp }${ body }` )
	.digest( 'hex' );

const response = await fetch( url, {
	method: 'POST',
	headers: {
		'Content-Type': 'application/json',
		'X-CS-Timestamp': String( timestamp ),
		'X-CS-Signature': signature
	},
	body
} );

if ( response.status === 201 ) {
	console.log( `Uploaded the editor bundle "${ editorConfig.cloudServices.bundleVersion }".` );
} else {
	console.error( `The upload failed with HTTP ${ response.status }:` );
	console.error( await response.text() );
	process.exitCode = 1;
}
Copy code

The script needs two environment variables:

  • CKEDITOR_CS_API_BASE_URL – The API base URL of your environment with the environment ID, for example https://ORGANIZATION.cke-cs.com/api/v5/ENVIRONMENT_ID.
  • CKEDITOR_CS_API_SECRET – An API secret of the environment.

Do not give these variables the VITE_ prefix, because Vite would put them in the application code. In CI, set them as secret variables. On your computer, you can put them in a .env.upload.local file, which the npm script below reads if it exists. Do not commit this file. The .gitignore file of the Vite templates ignores all files that end with .local.

Some details of the script:

  • It signs the exact body that it sends. Refer to the request signature guide for the algorithm.
  • It sends the shared configuration with the removePlugins option.
  • It removes the white space at the end of the test data file, for example the final new line that code editors add, because Cloud Services compares the data exactly.
  • The request body, with the bundle, the configuration, and the test data, must be smaller than 10 MB.

npm scripts

Copy link

Add the scripts for the bundle to the scripts section of package.json:

{
	// ...
	"scripts": {
		// ...
		"build:bundle": "vite build --config vite.bundle.config.js",
		"upload:bundle": "node --env-file-if-exists=.env.upload.local scripts/upload-bundle.js",
		"release:bundle": "npm run build:bundle && npm run upload:bundle"
	}
}
Copy code

Also add the bundle folder to .gitignore. Then git does not track the bundle, and the linter of the Vite template does not check it:

# The editor bundle for Cloud Services
dist-bundle
Copy code

To build and upload the bundle, run:

npm run release:bundle
Copy code

After a successful upload, the script prints Uploaded the editor bundle "react-vite-1.0.0". If you run it again with the same BUNDLE_VERSION, the upload fails with HTTP 409 and the Editor react-vite-1.0.0 already exists. message.

To see what Cloud Services stores, use the GET /editors/{bundle_version}/config request. The response shows the uploaded configuration and the plugins that Cloud Services adds to removePlugins.

Custom plugins

Copy link

Cloud Services runs the bundle without your application and does not use the editor UI. The uploaded configuration has no DOM elements, and all network requests from the editor during document conversions are blocked. Make your custom plugins ready for this:

  • Plugins that define data – A plugin that adds schema items, converters, or commands defines how the editor stores content. It must be in the bundle, and you must not remove it. In the example, this is PlaceholderEditing.
  • Plugins that only the browser needs – A plugin that sends requests, or shows UI that loads data from your server, must not run on the server. Add its name to pluginsRemovedOnServer. In the example, this is PlaceholderUI, which loads the placeholder types from the application server. Cloud Services removes some official plugins itself, for example the real-time collaboration plugins, Autosave, and the export and import plugins. The GET /editors/{bundle_version}/config request shows the full list.
  • Plugins that do both – Split them into an editing part and a UI part, as in the inline widget tutorial. List both parts in builtinPlugins. If a glue plugin requires a plugin that the upload removes, the upload fails with the plugincollection-required error.
  • Plugin names – The removePlugins option finds plugins by their pluginName. Give each custom plugin a static pluginName.
  • Plugins that need DOM elements – Put the DOM elements in the runtime configuration only, and make the plugin start without them. If the uploaded configuration must have such an option, set it to null. Do not send the element itself, because JSON.stringify() turns it into an empty object. For example, an empty object in sidebar.container stops the upload with the sidebar-setcontainer-invalid-element error.
  • Plugins that use React or other UI libraries – Keep the UI part out of the bundle if you can. If a library must be in the bundle, the define option of the bundle configuration handles process.env.NODE_ENV.

This is how the UI part of the placeholder feature gets its name and loads its data:

// src/editor/plugins/placeholder/placeholderui.js

// ...

// The UI part of the placeholder feature. It loads the placeholder types from the application server.
// Cloud Services does not need it and cannot reach that server, so the upload script removes it.
export class PlaceholderUI extends Plugin {
	static get pluginName() {
		return 'PlaceholderUI';
	}

	// ...

	async _loadTypes( items ) {
		const response = await fetch( this.editor.config.get( 'placeholderConfig.typesUrl' ) );

		// ...
	}
}
Copy code
Note

The Server-side Editor API does not use the removePlugins option of the upload. To remove plugins for a script, use the plugins_to_remove parameter of the request. Refer to the Editor scripts guide for details.

Updating the bundle

Copy link

When you change the editor, for example its plugins, its configuration, or the CKEditor 5 version:

  1. Change BUNDLE_VERSION in src/editor/config.js.
  2. Run npm run release:bundle.
  3. Deploy the application with the new version.

Then follow the steps in the Updating the editor bundle section of the main guide.

A collaboration session keeps the bundle version that created it. Editors with the old version can continue to work in it. An editor with the new version cannot connect to the session and fails with the realtimecollaborationclient-init-connection-failed error. The error details say: Incompatible bundle version or engine version detected. When the session ends or you remove it, the editor with the new version starts a new session. If document storage is on, the document data stays after you remove the session.

TypeScript

Copy link

The TypeScript variant of the project has the same files with the .ts and .tsx extensions. Make these changes:

  • Type the shared configuration with satisfies EditorConfig. Declare the types of custom options in a module augmentation, and import it in config.ts:

    // src/editor/config.ts
    
    import type { EditorConfig } from 'ckeditor5';
    
    // The types of the custom options, such as `placeholderConfig`. The file is empty at runtime.
    import './plugins/placeholder/augmentation.ts';
    
    // ...
    
    export const editorConfig = {
    	cloudServices: {
    		bundleVersion: BUNDLE_VERSION
    	},
    	// ...
    } satisfies EditorConfig;
    Copy code
    // src/editor/plugins/placeholder/augmentation.ts
    
    import type { PlaceholderCommand } from './placeholdercommand.ts';
    
    declare module 'ckeditor5' {
    	interface EditorConfig {
    
    		// The URL of a JSON array with the names of the placeholder types.
    		placeholderConfig?: {
    			typesUrl: string;
    		};
    	}
    
    	interface CommandsMap {
    		placeholder: PlaceholderCommand;
    	}
    }
    Copy code
  • Node.js 22.18 and later runs scripts/upload-bundle.ts directly, without a build step. Thus config.ts and the modules that it imports must use only syntax that Node.js can remove. They must also import from ckeditor5 only with import type. The react-ts template of Vite turns on the erasableSyntaxOnly compiler option, which checks this. Keep it on.

  • In scripts/upload-bundle.ts, import the configuration from ../src/editor/config.ts. The rest of the script is the same as in JavaScript.

  • In App.tsx, type the layout element and the containers. The element helper returns HTMLElement, because the configuration options do not accept null:

    // src/App.tsx
    
    // ...
    
    export default function App() {
    	// React gives the layout element to `setLayout` after it renders the containers of the editor UI.
    	const [ layout, setLayout ] = useState<HTMLDivElement | null>( null );
    
    	// ...
    }
    
    // ...
    function getRuntimeConfig( layout: HTMLElement ): EditorConfig {
    	// The containers exist, because React renders them before it gives the layout element to `setLayout`.
    	const element = ( selector: string ) => layout.querySelector<HTMLElement>( selector )!;
    
    	// ...
    }
    Copy code
  • Add the bundle configuration and the scripts to tsconfig.node.json:

    // tsconfig.node.json
    
    {
    	"compilerOptions": {
    		// ...
    	},
    	"include": ["vite.config.ts", "vite.bundle.config.ts", "scripts"]
    }
    Copy code
  • Change the npm scripts:

    {
    	// ...
    	"scripts": {
    		// ...
    		"build:bundle": "tsc -b && vite build --config vite.bundle.config.ts",
    		"upload:bundle": "node --env-file-if-exists=.env.upload.local scripts/upload-bundle.ts"
    	}
    }
    Copy code

Troubleshooting

Copy link

These are the most common errors of the upload.

Cloud Services returns HTTP 409 when the bundle, its configuration, or the test data has a problem:

  • Broken editor build. Details: Failed to execute 'appendChild' on 'Node': Unexpected token 'export'. – The bundle is an ES module. Build it with formats: [ 'umd' ].
  • Editor should be accessible under the `CKEditorCS` name. – The build.lib.name option is not CKEditorCS. Set name: 'CKEditorCS'.
  • Broken editor build. Details: CKEditorCS.create is not a function. – The entry file has other exports than the editor class. Export only the editor class. The exports: 'default' option stops such builds.
  • Broken editor build. Details: process is not defined. – A dependency in the bundle reads process.env. Add the define option to the bundle configuration.
  • Converted data result is not the same as provided in "test_data". – The editor output is different from test_data. The data.convertedData field of the response shows the output. Take the test data from editor.getData( { trim: 'none' } ) and do not change it. Refer to the Changes that the editor makes section of the test data guide.
  • Broken editor build. Details: sidebar-setcontainer-invalid-element – The uploaded configuration contains a DOM element, which JSON.stringify() turned into an empty object. Keep DOM elements in the runtime configuration, or set the option to null.
  • plugincollection-required {"plugin":"…","requiredBy":"…"} – A plugin in the bundle requires a plugin from removePlugins. List the parts of the feature in builtinPlugins and remove only the UI part.
  • Editor <version> already exists. – A bundle with this version exists. You cannot replace or delete it. Change BUNDLE_VERSION.

When the request itself has a problem, Cloud Services returns other status codes:

  • Validation failed. with CKEditor5 Editor Version must be at least 3 characters (HTTP 400) – The configuration has no cloudServices.bundleVersion. Set the bundle version. It must have at least 3 characters.
  • Invalid signature. (HTTP 401) – The API secret is wrong, or the signed data is not the same as the sent body. Sign the exact body string that you send.
  • Invalid request timestamp (HTTP 401) – The X-CS-Timestamp header differs by more than 15 minutes from the server time. Create the timestamp with Date.now() immediately before you send the request.
  • Payload too large. The maximum size is: 10 MB. (HTTP 413) – The request body is larger than 10 MB. Remove the plugins that your editor does not use, and use smaller test data.

If the upload is successful but the editor in the browser cannot connect with the realtimecollaborationclient-init-connection-failed error, refer to the Updating the bundle section.