ADR-002: Native CKEditor 5 vs Custom TYPO3 Image Plugin
- Status
-
Accepted
- Date
-
2025-11-09
- Authors
-
Development Team
- Context
-
RTE CKEditor Image Extension for TYPO3
Table of Contents
Context and Problem Statement
TYPO3 integrates CKEditor 5 as its Rich Text Editor (RTE), and images are a fundamental content element. CKEditor 5 provides comprehensive native image plugins (Image, ImageCaption, ImageToolbar, ImageResize, ImageStyle, LinkImage) with excellent WYSIWYG capabilities including:
- Inline editable captions
- Contextual toolbars on image click
- Visual resize handles
- Pre-defined image styles (alignment, sizing)
- Image linking capabilities
- Text alternative (alt) editing
However, TYPO3 has specific requirements for file handling through its File Abstraction Layer (FAL) that are incompatible with CKEditor 5's native image implementation.
The Question: Should we use CKEditor 5's native image plugins or implement a custom plugin specifically for TYPO3?
Decision Drivers
TYPO3 Core Requirements
- FAL Integration: All files must be managed through TYPO3's File Abstraction Layer
- Reference Tracking:
sys_file_referencedatabase records for all file usage - Magic Image Processing: TYPO3's automatic image optimization and variant generation
- Security: File access permissions and public/non-public file handling
- Backend Integration: File selector dialog and metadata management
User Experience Requirements
- WYSIWYG Editing: Inline caption editing, visual resize, contextual toolbars
- Accessibility: Alt text, semantic HTML structure
- Flexibility: Image styles, linking, sizing options
- Performance: Optimized image delivery
Technical Requirements
- Data Persistence: FAL attributes must survive RTE → DB → RTE round-trips
- Backend Rendering: PHP-based frontend rendering with TypoScript integration
- Compatibility: Work across TYPO3 versions (v12/v13)
Considered Options
Option 1: Use CKEditor 5 Native Image Plugins
Use the official CKEditor 5 image feature set:
@ckeditor/ckeditor5-image(base Image plugin)@ckeditor/ckeditor5-image/imagecaption(ImageCaption)@ckeditor/ckeditor5-image/imagetoolbar(ImageToolbar)@ckeditor/ckeditor5-image/imageresize(ImageResize)@ckeditor/ckeditor5-image/imagestyle(ImageStyle)@ckeditor/ckeditor5-link/linkimage(LinkImage)
Pros:
- ✅ Excellent WYSIWYG experience out-of-the-box
- ✅ Inline editable captions with proper UX
- ✅ Contextual balloon toolbar on image selection
- ✅ Visual resize handles with dimension control
- ✅ Pre-built image styles (alignment, sizing)
- ✅ Official support and documentation
- ✅ Regular updates and bug fixes
- ✅ Accessibility features built-in
- ✅ Tested across browsers and devices
Cons:
- ❌ No FAL Support: Uses direct image URLs (
srcattribute), not FAL file references - ❌ No Reference Tracking: Cannot create
sys_file_referencerecords - ❌ No Magic Image Processing: Cannot integrate with TYPO3's image processing pipeline
- ❌ No Backend Integration: Cannot use TYPO3's file selector dialog
- ❌ Data Attribute Loss: Cannot store FAL-specific metadata (
data-htmlarea-file-uid,data-htmlarea-file-table) - ❌ Security Issues: Cannot enforce TYPO3's file access permissions
- ❌ Frontend Rendering Incompatibility: PHP rendering expects FAL attributes, not direct URLs
- ❌ TYPO3 Core Explicitly Disables It: Core configuration removes the plugin
Option 2: Custom TYPO3 Image Plugin (Selected)
Implement a custom CKEditor 5 plugin (typo3image) that:
- Uses custom model element (
typo3imageinstead ofimageBlock) - Stores FAL attributes in the model
- Integrates with TYPO3's file selector dialog
- Provides backend rendering through PHP controllers
- Supports magic image processing
Pros:
- ✅ Full FAL Integration: Stores file UID and table references
- ✅ Reference Tracking: Creates proper
sys_file_referencerecords - ✅ Magic Image Processing: Full integration with TYPO3's image pipeline
- ✅ Backend Integration: Uses TYPO3's file selector dialog
- ✅ Data Persistence: All FAL attributes preserved through save/load cycles
- ✅ Security: Respects TYPO3's file access permissions
- ✅ Frontend Rendering: PHP controllers handle all rendering logic
- ✅ TypoScript Integration: Full control via TypoScript configuration
- ✅ Feature Control: Can implement exactly what TYPO3 needs
Cons:
- ❌ Must implement WYSIWYG features manually (captions, toolbar, resize, styles)
- ❌ Higher development and maintenance effort
- ❌ Requires deep CKEditor 5 plugin architecture knowledge
- ❌ Must keep up with CKEditor 5 API changes
- ❌ More complex than using native plugins
Option 3: Hybrid Approach (Native Plugins + FAL Bridge)
Use native CKEditor 5 plugins but add a bridge layer to convert to/from FAL.
Pros:
- ✅ WYSIWYG features from native plugins
- ✅ Potential FAL integration through conversion
Cons:
- ❌ Model Incompatibility: Native
imageBlockmodel doesn't support FAL attributes - ❌ Data Loss Risk: Conversion between incompatible models prone to errors
- ❌ Plugin Conflicts: Native plugins expect specific model schema
- ❌ Maintenance Nightmare: Must bridge two incompatible architectures
- ❌ TYPO3 Core Still Disables Native Plugin: Cannot be used without core modifications
Decision Outcome
Chosen option: Option 2 - Custom TYPO3 Image Plugin
The custom plugin approach is the only viable option for TYPO3 integration.
Evidence and Technical Justification
1. TYPO3 Core Explicitly Disables Native Image Plugin
File: typo3/sysext/rte_ckeditor/Configuration/RTE/Default.yaml
editor:
config:
removePlugins:
- image # ← Native Image plugin is disabled
Source: TYPO3 Core Documentation
"By default, images in CKE are disabled within configuration typo3/sysext/rte_ckeditor/Configuration/RTE/Default.yaml with removePlugins: - image"
This is not accidental — it's a deliberate architectural decision by the TYPO3 core team.
2. FAL Requirements Incompatible with Native Plugins
Required FAL Data Attributes
<img
src="fileadmin/user_upload/image.jpg"
data-htmlarea-file-uid="123"
data-htmlarea-file-table="sys_file"
alt="Example"
width="800"
height="600"
/>
Why These Matter:
data-htmlarea-file-uid: Links tosys_filerecord for reference trackingdata-htmlarea-file-table: Specifies FAL table (alwayssys_file)- These attributes trigger
sys_file_referencecreation in TYPO3 backend - Without them, TYPO3 cannot track where files are used
- Reference index breaks, file deletion safety checks fail
Native Plugin Model
// CKEditor 5 native imageBlock model
editor.model.schema.register('imageBlock', {
allowAttributes: ['src', 'alt', 'srcset', 'sizes']
// ❌ No support for data-* attributes
// ❌ No FAL metadata storage
});
3. Magic Image Processing Requirements
Frontend Rendering Flow:
1. RTE saves: <img data-htmlarea-file-uid="123" width="800" height="600" />
2. TYPO3 Backend: Creates sys_file_reference record
3. Frontend Rendering (PHP):
a. Load file from FAL via UID
b. Apply TypoScript configuration
c. Generate processed image variant
d. Output: <img src="/typo3temp/processed/image_hash.jpg" />
TypoScript Hook:
lib.parseFunc_RTE {
tags.img = TEXT
tags.img {
preUserFunc = Netresearch\RteCKEditorImage\Controller\ImageRenderingController->renderImageAttributes
}
}
Why Native Plugins Can't Support This:
- Native plugins use direct
srcURLs, not FAL UIDs ImageRenderingController->renderImageAttributes()expects FAL data attributes- No way to look up file in FAL without UID
- Cannot apply magic image processing without FAL context
4. Backend Integration Requirements
TYPO3 File Selector Dialog:
externalPlugins: {
typo3image: {
route: "rteckeditorimage_wizard_select_image" // ← TYPO3 backend route
}
}
Flow:
- User clicks "Insert Image" button
- Custom plugin opens TYPO3's file selector dialog
- User selects file from FAL storage
- Dialog returns
{ fileUid: 123, fileTable: 'sys_file', ... } - Plugin creates
typo3imagemodel element with FAL attributes
Native Plugin Limitation:
- Native
uploadImageplugin expects file upload or URL input - No integration point for TYPO3's file selector
- Cannot access FAL metadata
- Cannot select existing files from storage
5. Database Reference Tracking
sys_file_reference Table:
CREATE TABLE sys_file_reference (
uid_local int, -- Points to sys_file.uid
uid_foreign int, -- Points to tt_content.uid
tablenames varchar, -- 'tt_content'
fieldname varchar -- 'bodytext'
);
Created by:
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][]
= RteImagesDbHook::class;
Why This Matters:
- TYPO3's "Used on" feature shows where images are referenced
- Prevents deleting images still in use
- File usage statistics
- Broken link detection
- Requires FAL UIDs — native plugins provide only URLs
Implementation: Custom Plugin Architecture
Model Schema
editor.model.schema.register('typo3image', {
inheritAllFrom: '$blockObject',
allowAttributes: [
'fileUid', // FAL: sys_file.uid
'fileTable', // FAL: table name (sys_file)
'src', // Image URL (for editing preview)
'alt', // Accessibility
'title', // Image title
'width', // Display dimensions
'height', // Display dimensions
'enableZoom', // Click-to-enlarge
'noScale', // Skip image processing
'quality', // Processing quality multiplier
'caption', // Image caption text
'htmlA', // Link wrapper attributes
// ... more FAL-specific attributes
]
});
Upcast Converter (HTML → Model)
editor.conversion.for('upcast').elementToElement({
view: {
name: 'img',
attributes: {
'data-htmlarea-file-uid': true // ← FAL attribute required
}
},
model: (viewElement, { writer }) => {
return writer.createElement('typo3image', {
fileUid: viewElement.getAttribute('data-htmlarea-file-uid'),
fileTable: viewElement.getAttribute('data-htmlarea-file-table'),
src: viewElement.getAttribute('src'),
// ... extract all FAL attributes
});
}
});
Downcast Converter (Model → HTML)
editor.conversion.for('dataDowncast').elementToElement({
model: 'typo3image',
view: (modelElement, { writer }) => {
const img = writer.createEmptyElement('img', {
'src': modelElement.getAttribute('src'),
'data-htmlarea-file-uid': modelElement.getAttribute('fileUid'),
'data-htmlarea-file-table': modelElement.getAttribute('fileTable'),
// ... output all FAL attributes
});
return toWidget(figure, writer);
}
});
Frontend Rendering
public function renderImageAttributes(string $content, array $conf): string
{
// Extract data-htmlarea-file-uid from HTML
$fileUid = $this->extractFileUid($content);
// Load file from FAL
$file = $this->resourceFactory->getFileObject($fileUid);
// Apply magic image processing
$processedImage = $this->imageService->applyProcessingInstructions($file, [
'width' => $width,
'height' => $height,
'crop' => $cropData,
]);
// Generate <img> tag with processed URL
return sprintf('<img src="%s" alt="%s" />',
$processedImage->getPublicUrl(),
$alt
);
}
Consequences
Positive
- ✅ Full TYPO3 Integration: Complete FAL support, reference tracking, magic image processing
- ✅ Security: Respects TYPO3's file permission system
- ✅ Flexibility: Can implement exactly what TYPO3 needs, no compromises
- ✅ TypoScript Control: Full configuration via TypoScript
- ✅ Future-Proof: Can add TYPO3-specific features without CKEditor limitations
- ✅ Backward Compatibility: Works with existing TYPO3 content and workflows
Negative
- ❌ WYSIWYG Features Missing: Must implement caption editing, contextual toolbar, visual resize, image styles manually
- ❌ Development Effort: Significant implementation work for WYSIWYG features
- ❌ Maintenance Burden: Must track CKEditor 5 API changes and update accordingly
- ❌ Feature Parity Challenge: Native plugins have better UX, must match quality
- ❌ Knowledge Requirements: Deep CKEditor 5 plugin architecture expertise needed
Current Limitations
The custom plugin currently lacks some WYSIWYG features present in native plugins:
-
Caption Not Inline Editable: Uses dialog instead of click-to-edit
- Root cause:
toWidget()wrapper prevents nested editables - Status: Implementation gap - can be addressed with proper editable nesting
- Root cause:
-
No Contextual Toolbar: No balloon toolbar on image selection
- ✅ IMPLEMENTED in feature/wysiwyg-caption-fixes branch via WidgetToolbarRepository
-
No Visual Resize: No drag handles for resizing
- Root cause: WidgetResize is NOT available in TYPO3's CKEditor 5 build (confirmed in typo3image.js:1576)
- Status: Architectural limitation - cannot be implemented without CKEditor build changes
- Alternative: Resize functionality available via context toolbar buttons
-
No Image Styles: No pre-defined alignment/sizing options
- ✅ IMPLEMENTED via balloon toolbar (alignment buttons: left/center/right/block)
Summary: Most gaps are implementation issues that can be addressed. Visual resize handles are an architectural limitation due to WidgetResize unavailability in TYPO3's CKEditor 5 build.
Compliance
- TYPO3 Core Architecture: Follows FAL (File Abstraction Layer) patterns
- CKEditor 5 Plugin API: Implements official plugin architecture
- Security: File access control and permission checking
- Performance: Optimized image processing pipeline
- Accessibility: Semantic HTML structure support
References
- TYPO3 FAL Documentation
- TYPO3 RTE Configuration
- CKEditor 5 Image Feature
- CKEditor 5 Plugin Development
- TYPO3 Core RTE Default Configuration:
typo3/sysext/rte_ckeditor/Configuration/RTE/Default.yaml
Future Considerations
While native CKEditor 5 plugins cannot be used directly, their implementation patterns can guide our custom plugin development:
- Study
ImageCaptionsource for inline editable caption UX - Study
ImageToolbarsource for contextual balloon toolbar - Study
ImageResizesource for visual resize handle implementation - Study
ImageStylesource for style dropdown integration
Goal: Achieve feature parity with native plugins while maintaining full TYPO3 FAL integration.
Revision History
| Date | Version | Changes |
|---|---|---|
| 2025-11-09 | 1.0 | Initial ADR documenting why native CKEditor 5 plugins cannot be used with TYPO3 FAL |