TYPO3 Content Blocks Development
Compatibility: TYPO3 v13.x and v14.x (v14 preferred) All code examples in this skill are designed to work on both TYPO3 v13 and v14.
1. The Single Source of Truth Principle
Content Blocks is the modern approach to creating custom content types in TYPO3. It eliminates redundancy by providing a single YAML configuration that generates:
- TCA (Table Configuration Array)
- Database schema (SQL)
- TypoScript rendering
- Backend forms and previews
- Labels and translations
Why Content Blocks?
| Traditional Approach | Content Blocks Approach |
|---|---|
| Multiple TCA files | One config.yaml |
| Manual SQL definitions | Auto-generated schema |
| Separate TypoScript | Auto-registered rendering |
| Scattered translations | Single labels.xlf |
| Complex setup | Simple folder structure |
2. Installation
# Install via Composer (DDEV recommended)
ddev composer require friendsoftypo3/content-blocks
# After installation, clear caches
ddev typo3 cache:flush
Security Configuration (Classic Mode)
For non-composer installations, deny web access to ContentBlocks folder:
# .htaccess addition
RewriteRule (?:typo3conf/ext|typo3/sysext|typo3/ext)/[^/]+/(?:Configuration|ContentBlocks|Resources/Private|Tests?|Documentation|docs?)/ - [F]
3. Content Types Overview
Content Blocks supports four content types:
| Type | Folder | Use Case |
|---|---|---|
ContentElements |
ContentBlocks/ContentElements/ |
Frontend content (tt_content) |
RecordTypes |
ContentBlocks/RecordTypes/ |
Custom records (new tables) |
PageTypes |
ContentBlocks/PageTypes/ |
Custom page types |
FileTypes |
ContentBlocks/FileTypes/ |
Custom file metadata |
4. Folder Structure
EXT:my_sitepackage/
└── ContentBlocks/
├── ContentElements/
│ └── my-hero/
│ ├── assets/
│ │ └── icon.svg
│ ├── language/
│ │ └── labels.xlf
│ ├── templates/
│ │ ├── backend-preview.html
│ │ ├── frontend.html
│ │ └── partials/
│ └── config.yaml
└── RecordTypes/
└── my-record/
├── assets/
│ └── icon.svg
├── language/
│ └── labels.xlf
└── config.yaml
5. Creating Content Elements
Kickstart Command (Recommended)
# Interactive mode
ddev typo3 make:content-block
# One-liner
ddev typo3 make:content-block \
--content-type="content-element" \
--vendor="myvendor" \
--name="hero-banner" \
--title="Hero Banner" \
--extension="my_sitepackage"
# After creation, update database
ddev typo3 cache:flush -g system
ddev typo3 extension:setup --extension=my_sitepackage
Minimal Content Element
# EXT:my_sitepackage/ContentBlocks/ContentElements/hero-banner/config.yaml
name: myvendor/hero-banner
fields:
- identifier: header
useExistingField: true
- identifier: bodytext
useExistingField: true
Full Content Element Example
# EXT:my_sitepackage/ContentBlocks/ContentElements/hero-banner/config.yaml
name: myvendor/hero-banner
group: default
description: "A full-width hero banner with image and CTA"
prefixFields: true
prefixType: full
basics:
- TYPO3/Appearance
- TYPO3/Links
fields:
- identifier: header
useExistingField: true
- identifier: subheadline
type: Text
label: Subheadline
- identifier: hero_image
type: File
minitems: 1
maxitems: 1
allowed: common-image-types
- identifier: cta_link
type: Link
label: Call to Action Link
- identifier: cta_text
type: Text
label: Button Text
Frontend Template
<!-- templates/frontend.html -->
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:cb="http://typo3.org/ns/TYPO3/CMS/ContentBlocks/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:asset.css identifier="hero-banner-css" href="{cb:assetPath()}/frontend.css"/>
<section class="hero-banner">
<f:if condition="{data.hero_image}">
<f:for each="{data.hero_image}" as="image">
<f:image image="{image}" alt="{data.header}" class="hero-image"/>
</f:for>
</f:if>
<div class="hero-content">
<h1>{data.header}</h1>
<f:if condition="{data.subheadline}">
<p class="subheadline">{data.subheadline}</p>
</f:if>
<f:if condition="{data.cta_link}">
<f:link.typolink parameter="{data.cta_link}" class="btn btn-primary">
{data.cta_text -> f:or(default: 'Learn more')}
</f:link.typolink>
</f:if>
</div>
</section>
</html>
Backend Preview Template
<!-- templates/backend-preview.html -->
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:be="http://typo3.org/ns/TYPO3/CMS/Backend/ViewHelpers"
data-namespace-typo3-fluid="true">
<div class="content-block-preview">
<strong>{data.header}</strong>
<f:if condition="{data.subheadline}">
<br/><em>{data.subheadline}</em>
</f:if>
<f:if condition="{data.hero_image}">
<f:for each="{data.hero_image}" as="image">
<be:thumbnail image="{image}" width="100" height="100"/>
</f:for>
</f:if>
</div>
</html>
6. Creating Record Types (Custom Tables)
Record Types create custom database tables for structured data like teams, products, events, etc.
Extbase-Compatible Table Naming
IMPORTANT: For Extbase compatibility, use the tx_extensionkey_domain_model_* naming convention:
# ✅ CORRECT - Extbase compatible table name
name: myvendor/team-member
table: tx_mysitepackage_domain_model_teammember
labelField: name
fields:
- identifier: name
type: Text
- identifier: position
type: Text
- identifier: email
type: Email
- identifier: photo
type: File
allowed: common-image-types
maxitems: 1
# ❌ WRONG - Short table names don't work with Extbase
name: myvendor/team-member
table: team_member # Won't work with Extbase!
Minimal Record Type
# EXT:my_sitepackage/ContentBlocks/RecordTypes/team-member/config.yaml
name: myvendor/team-member
table: tx_mysitepackage_domain_model_teammember
labelField: name
fields:
- identifier: name
type: Text
Full Record Type Example
# EXT:my_sitepackage/ContentBlocks/RecordTypes/team-member/config.yaml
name: myvendor/team-member
table: tx_mysitepackage_domain_model_teammember
labelField: name
fallbackLabelFields:
- email
languageAware: true
workspaceAware: true
sortable: true
softDelete: true
trackCreationDate: true
trackUpdateDate: true
internalDescription: true
restriction:
disabled: true
startTime: true
endTime: true
security:
ignorePageTypeRestriction: true # Allow on normal pages
fields:
- identifier: name
type: Text
required: true
- identifier: position
type: Text
- identifier: email
type: Email
- identifier: phone
type: Text
- identifier: bio
type: Textarea
enableRichtext: true
- identifier: photo
type: File
allowed: common-image-types
maxitems: 1
- identifier: social_links
type: Collection
labelField: platform
fields:
- identifier: platform
type: Select
items:
- label: LinkedIn
value: linkedin
- label: Twitter/X
value: twitter
- label: GitHub
value: github
- identifier: url
type: Link
Multi-Type Records (Single Table Inheritance)
Create multiple types for one table:
# EXT:my_sitepackage/ContentBlocks/RecordTypes/person-employee/config.yaml
name: myvendor/person-employee
table: tx_mysitepackage_domain_model_person
typeField: person_type
typeName: employee
priority: 999 # Default type (loaded first)
labelField: name
languageAware: false
workspaceAware: false
fields:
- identifier: name
type: Text
- identifier: department
type: Text
# EXT:my_sitepackage/ContentBlocks/RecordTypes/person-contractor/config.yaml
name: myvendor/person-contractor
table: tx_mysitepackage_domain_model_person
typeName: contractor
fields:
- identifier: name
type: Text
- identifier: company
type: Text
- identifier: contract_end
type: DateTime
Record Types as Collection Children
Define a record that can be used in IRRE collections:
# EXT:my_sitepackage/ContentBlocks/RecordTypes/slide/config.yaml
name: myvendor/slide
table: tx_mysitepackage_domain_model_slide
labelField: title
fields:
- identifier: title
type: Text
- identifier: image
type: File
maxitems: 1
- identifier: link
type: Link
# EXT:my_sitepackage/ContentBlocks/ContentElements/slider/config.yaml
name: myvendor/slider
fields:
- identifier: slides
type: Collection
foreign_table: tx_mysitepackage_domain_model_slide
shareAcrossTables: true
shareAcrossFields: true
minitems: 1
7. Field Types Reference
Simple Fields
| Type | Description | Example |
|---|---|---|
Text |
Single line text | type: Text |
Textarea |
Multi-line text | type: Textarea |
Email |
Email address | type: Email |
Link |
Link/URL | type: Link |
Number |
Integer/Float | type: Number |
DateTime |
Date and/or time | type: DateTime |
Color |
Color picker | type: Color |
Checkbox |
Boolean checkbox | type: Checkbox |
Radio |
Radio buttons | type: Radio |
Slug |
URL slug | type: Slug |
Password |
Password field | type: Password |
Relational Fields
| Type | Description | Example |
|---|---|---|
File |
File references (FAL) | type: File |
Relation |
Record relations | type: Relation |
Select |
Dropdown selection | type: Select |
Category |
System categories | type: Category |
Collection |
Inline records (IRRE) | type: Collection |
Folder |
Folder reference | type: Folder |
Language |
Language selector | type: Language |
Structural Fields
| Type | Description | Example |
|---|---|---|
Tab |
Tab separator | type: Tab |
Palette |
Group fields | type: Palette |
Linebreak |
Line break in palette | type: Linebreak |
FlexForm |
FlexForm container | type: FlexForm |
Json |
JSON field | type: Json |
Common Field Options
fields:
- identifier: my_field
type: Text
label: My Field Label # Static label (or use labels.xlf)
description: Help text # Field description
required: true # Make field required
default: "Default value" # Default value
placeholder: "Enter text..." # Placeholder text
prefixField: false # Disable prefixing for this field
useExistingField: true # Reuse existing TCA field
displayCond: 'FIELD:other:=:1' # Conditional display
onChange: reload # Reload form on change
File Field Example
fields:
- identifier: gallery_images
type: File
allowed: common-image-types
minitems: 1
maxitems: 10
appearance:
createNewRelationLinkTitle: Add Image
showAllLocalizationLink: true
behaviour:
allowLanguageSynchronization: true
Select Field Example
fields:
- identifier: layout
type: Select
renderType: selectSingle
default: default
items:
- label: Default Layout
value: default
- label: Wide Layout
value: wide
- label: Compact Layout
value: compact
Collection Field Example (Inline IRRE)
fields:
- identifier: accordion_items
type: Collection
labelField: title
minitems: 1
maxitems: 20
appearance:
collapseAll: true
levelLinksPosition: both
fields:
- identifier: title
type: Text
required: true
- identifier: content
type: Textarea
enableRichtext: true
- identifier: is_open
type: Checkbox
label: Initially Open
8. Field Prefixing
Content Blocks automatically prefixes field identifiers to avoid collisions.
Prefixing Types
# Full prefix (default): myvendor_myblock_fieldname
name: myvendor/my-block
prefixFields: true
prefixType: full
# Vendor prefix only: myvendor_fieldname
name: myvendor/my-block
prefixFields: true
prefixType: vendor
# Custom vendor prefix: tx_custom_fieldname
name: myvendor/my-block
prefixFields: true
prefixType: vendor
vendorPrefix: tx_custom
# No prefix (use with caution!)
name: myvendor/my-block
prefixFields: false
Disable Prefixing per Field
fields:
- identifier: my_custom_field
type: Text
prefixField: false # This field won't be prefixed
9. Templating Features
Accessing Data in Fluid
<!-- Basic field access -->
{data.header}
{data.my_field}
<!-- Record metadata -->
{data.uid}
{data.pid}
{data.languageId}
{data.mainType} <!-- Table name: tt_content -->
{data.recordType} <!-- CType: myvendor_heroblock -->
{data.fullType} <!-- tt_content.myvendor_heroblock -->
<!-- Raw database values -->
{data.rawRecord.some_field}
<!-- System properties -->
{data.systemProperties.createdAt}
{data.systemProperties.lastUpdatedAt}
{data.systemProperties.sorting}
{data.systemProperties.disabled}
<!-- Language info -->
{data.languageInfo.translationParent}
{data.languageInfo.translationSource}
<!-- Relations are auto-resolved! -->
<f:for each="{data.gallery_images}" as="image">
<f:image image="{image}" width="400"/>
</f:for>
<!-- Nested collections -->
<f:for each="{data.accordion_items}" as="item">
<h3>{item.title}</h3>
<f:format.html>{item.content}</f:format.html>
</f:for>
Asset ViewHelpers
<!-- Include CSS from assets folder -->
<f:asset.css identifier="my-block-css" href="{cb:assetPath()}/frontend.css"/>
<!-- Include JS from assets folder -->
<f:asset.script identifier="my-block-js" src="{cb:assetPath()}/frontend.js"/>
<!-- Cross-block asset reference -->
<f:asset.css identifier="shared-css" href="{cb:assetPath(name: 'vendor/other-block')}/shared.css"/>
Translation ViewHelper
<!-- Access labels.xlf translations -->
<f:translate key="{cb:languagePath()}:my_label"/>
<!-- Cross-block translation -->
<f:translate key="{cb:languagePath(name: 'vendor/other-block')}:shared_label"/>
10. Extending Existing Tables
Add custom types to existing tables (like tx_news):
# EXT:my_sitepackage/ContentBlocks/RecordTypes/custom-news/config.yaml
name: myvendor/custom-news
table: tx_news_domain_model_news
typeName: custom_news
fields:
- identifier: title
useExistingField: true
- identifier: custom_field
type: Text
11. Workflow with DDEV
Standard Development Workflow
# 1. Create new Content Block
ddev typo3 make:content-block
# 2. Clear system caches
ddev typo3 cache:flush -g system
# 3. Update database schema
ddev typo3 extension:setup --extension=my_sitepackage
# Alternative: Use Database Analyzer in TYPO3 Backend
# Admin Tools > Maintenance > Analyze Database Structure
Using webprofil/make Extension
If webprofil/make is installed:
# Create Content Block with webprofil/make
ddev make:content_blocks
# Clear caches and update database
ddev typo3 cache:flush
ddev typo3 database:updateschema
Integration with Extbase
After creating Record Types with proper table names, generate Extbase models:
# If typo3:make:model is available
ddev typo3 make:model --extension=my_sitepackage
# Generate repository
ddev typo3 make:repository --extension=my_sitepackage
12. Defaults Configuration
Create a content-blocks.yaml in project root for default settings:
# content-blocks.yaml
vendor: myvendor
extension: my_sitepackage
content-type: content-element
skeleton-path: content-blocks-skeleton
config:
content-element:
basics:
- TYPO3/Appearance
- TYPO3/Links
group: common
prefixFields: true
prefixType: full
record-type:
prefixFields: true
prefixType: vendor
vendorPrefix: tx_mysitepackage
13. Best Practices
DO ✅
Use Extbase-compatible table names for Record Types:
table: tx_myextension_domain_model_myrecordReuse existing fields when possible:
- identifier: header useExistingField: trueGroup related fields with Tabs and Palettes:
- identifier: settings_tab type: Tab label: SettingsUse meaningful identifiers (snake_case):
- identifier: hero_background_imageClear caches after changes:
ddev typo3 cache:flush -g system ddev typo3 extension:setup --extension=my_sitepackageUse labels.xlf for all user-facing labels
DON'T ❌
Don't use raw SQL - Content Blocks generates schema automatically
Don't duplicate TCA - Config.yaml is the single source of truth
Don't use short table names for Extbase integration:
# ❌ Wrong table: team_member # ✅ Correct table: tx_mysitepackage_domain_model_teammemberDon't use dashes in identifiers:
# ❌ Wrong identifier: hero-image # ✅ Correct identifier: hero_imageDon't forget shareAcross options when using foreign_table in multiple places
14. Troubleshooting
Content Block Not Appearing
# Clear all caches
ddev typo3 cache:flush
# Rebuild class loading
ddev composer dump-autoload
# Check extension setup
ddev typo3 extension:setup --extension=my_sitepackage
Database Errors
# Update database schema
ddev typo3 database:updateschema
# Or use Compare Tool
# Admin Tools > Maintenance > Analyze Database Structure
Field Not Saving
- Check field identifier is unique (use prefixing)
- Verify field type is correct
- Check for typos in config.yaml
- Ensure labels.xlf has matching keys
15. Version Constraints
// ext_emconf.php
$EM_CONF[$_EXTKEY] = [
'title' => 'My Extension',
'version' => '1.0.0',
'state' => 'stable',
'constraints' => [
'depends' => [
'typo3' => '13.0.0-14.99.99',
'content_blocks' => '2.0.0-2.99.99',
],
],
];
16. Migrating Classic Extensions to Content Blocks
This section guides you through converting traditional TYPO3 extensions (with separate TCA, SQL, TypoScript) to the modern Content Blocks approach.
When to Migrate
| Scenario | Recommendation |
|---|---|
| New content elements | ✅ Use Content Blocks from the start |
| Simple records (products, team, events) | ✅ Migrate to Content Blocks |
| Complex Extbase extensions with controllers | ⚠️ Keep Extbase, optionally use Content Blocks for TCA |
| Heavy business logic in domain models | ⚠️ Keep Extbase models, consider Content Blocks for forms only |
| Extensions with many plugins | ❌ Keep traditional approach |
Migration Strategy
┌─────────────────────────────────────────────────────────────────┐
│ 1. ANALYZE │
│ └─ Identify TCA, SQL, TypoScript, Templates │
├─────────────────────────────────────────────────────────────────┤
│ 2. MAP │
│ └─ Create field mapping from TCA columns to Content Blocks │
├─────────────────────────────────────────────────────────────────┤
│ 3. CREATE │
│ └─ Build config.yaml with mapped fields │
├─────────────────────────────────────────────────────────────────┤
│ 4. MIGRATE DATA │
│ └─ Rename columns if needed, update CTypes │
├─────────────────────────────────────────────────────────────────┤
│ 5. CLEANUP │
│ └─ Remove old TCA, SQL, TypoScript files │
└─────────────────────────────────────────────────────────────────┘
TCA to Content Blocks Field Mapping
| TCA Type | TCA renderType | Content Blocks Type | Notes |
|---|---|---|---|
input |
- | Text |
Basic text input |
input |
inputDateTime |
DateTime |
Date/time picker |
input |
inputLink |
Link |
Link browser |
input |
colorPicker |
Color |
Color picker |
input |
slug |
Slug |
URL slug |
text |
- | Textarea |
Multi-line text |
text |
(richtext) | Textarea + enableRichtext: true |
RTE |
check |
- | Checkbox |
Boolean checkbox |
radio |
- | Radio |
Radio buttons |
select |
selectSingle |
Select |
Single selection |
select |
selectMultipleSideBySide |
Select + multiple: true |
Multiple selection |
select |
selectCheckBox |
Select + renderType: selectCheckBox |
Checkbox group |
group |
- | Relation or File |
Depends on internal_type |
file |
- | File |
FAL references |
inline |
- | Collection |
IRRE relations |
category |
- | Category |
System categories |
flex |
- | FlexForm |
FlexForm container |
json |
- | Json |
JSON data |
Migration Example 1: Content Element (tt_content)
BEFORE (Classic):
// Configuration/TCA/Overrides/tt_content.php
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPlugin(
['LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:ce.hero', 'myext_hero'],
'CType',
'my_ext'
);
$GLOBALS['TCA']['tt_content']['types']['myext_hero'] = [
'showitem' => '
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
--palette--;;general,
header,
tx_myext_subheadline,
tx_myext_image,
tx_myext_link,
--div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.appearance,
--palette--;;frames,
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
--palette--;;hidden,
',
];
$tempColumns = [
'tx_myext_subheadline' => [
'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:subheadline',
'config' => [
'type' => 'input',
'size' => 50,
'max' => 255,
],
],
'tx_myext_image' => [
'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:image',
'config' => [
'type' => 'file',
'maxitems' => 1,
'allowed' => 'common-image-types',
],
],
'tx_myext_link' => [
'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:link',
'config' => [
'type' => 'link',
],
],
];
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('tt_content', $tempColumns);
-- ext_tables.sql
CREATE TABLE tt_content (
tx_myext_subheadline varchar(255) DEFAULT '' NOT NULL,
tx_myext_image int(11) DEFAULT 0 NOT NULL,
tx_myext_link varchar(1024) DEFAULT '' NOT NULL
);
# Configuration/TypoScript/setup.typoscript
tt_content.myext_hero = FLUIDTEMPLATE
tt_content.myext_hero {
templateName = Hero
templateRootPaths.10 = EXT:my_ext/Resources/Private/Templates/
}
AFTER (Content Blocks):
# ContentBlocks/ContentElements/hero/config.yaml
name: myvendor/hero
basics:
- TYPO3/Appearance
- TYPO3/Links
fields:
- identifier: header
useExistingField: true
- identifier: subheadline
type: Text
- identifier: image
type: File
maxitems: 1
allowed: common-image-types
- identifier: link
type: Link
<!-- ContentBlocks/ContentElements/hero/templates/frontend.html -->
<section class="hero">
<f:if condition="{data.image}">
<f:for each="{data.image}" as="img">
<f:image image="{img}" class="hero-bg"/>
</f:for>
</f:if>
<h1>{data.header}</h1>
<f:if condition="{data.subheadline}">
<p>{data.subheadline}</p>
</f:if>
<f:if condition="{data.link}">
<f:link.typolink parameter="{data.link}" class="btn">Learn More</f:link.typolink>
</f:if>
</section>
That's it! No TCA files, no SQL, no TypoScript for rendering.
Migration Example 2: Custom Record Table
BEFORE (Classic):
// Configuration/TCA/tx_myext_domain_model_product.php
return [
'ctrl' => [
'title' => 'Product',
'label' => 'name',
'tstamp' => 'tstamp',
'crdate' => 'crdate',
'delete' => 'deleted',
'sortby' => 'sorting',
'languageField' => 'sys_language_uid',
'transOrigPointerField' => 'l10n_parent',
'transOrigDiffSourceField' => 'l10n_diffsource',
'enablecolumns' => [
'disabled' => 'hidden',
'starttime' => 'starttime',
'endtime' => 'endtime',
],
'iconfile' => 'EXT:my_ext/Resources/Public/Icons/product.svg',
],
'columns' => [
'hidden' => [
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.hidden',
'config' => ['type' => 'check'],
],
'name' => [
'label' => 'Name',
'config' => [
'type' => 'input',
'size' => 50,
'max' => 255,
'required' => true,
],
],
'description' => [
'label' => 'Description',
'config' => [
'type' => 'text',
'enableRichtext' => true,
],
],
'price' => [
'label' => 'Price',
'config' => [
'type' => 'number',
'format' => 'decimal',
],
],
'sku' => [
'label' => 'SKU',
'config' => [
'type' => 'input',
'size' => 30,
],
],
'image' => [
'label' => 'Image',
'config' => [
'type' => 'file',
'maxitems' => 5,
'allowed' => 'common-image-types',
],
],
'category' => [
'label' => 'Category',
'config' => [
'type' => 'select',
'renderType' => 'selectSingle',
'items' => [
['label' => 'Electronics', 'value' => 'electronics'],
['label' => 'Clothing', 'value' => 'clothing'],
['label' => 'Books', 'value' => 'books'],
],
],
],
// ... more system fields
],
'types' => [
'1' => ['showitem' => 'hidden, name, sku, price, category, description, image'],
],
];
-- ext_tables.sql
CREATE TABLE tx_myext_domain_model_product (
name varchar(255) DEFAULT '' NOT NULL,
description text,
price double(11,2) DEFAULT 0.00 NOT NULL,
sku varchar(100) DEFAULT '' NOT NULL,
image int(11) DEFAULT 0 NOT NULL,
category varchar(50) DEFAULT '' NOT NULL
);
AFTER (Content Blocks):
# ContentBlocks/RecordTypes/product/config.yaml
name: myvendor/product
table: tx_myext_domain_model_product
labelField: name
languageAware: true
workspaceAware: false
sortable: true
softDelete: true
trackCreationDate: true
trackUpdateDate: true
restriction:
disabled: true
startTime: true
endTime: true
security:
ignorePageTypeRestriction: true
fields:
- identifier: name
type: Text
required: true
- identifier: sku
type: Text
- identifier: price
type: Number
format: decimal
- identifier: category
type: Select
items:
- label: Electronics
value: electronics
- label: Clothing
value: clothing
- label: Books
value: books
- identifier: description
type: Textarea
enableRichtext: true
- identifier: image
type: File
maxitems: 5
allowed: common-image-types
That's it! No TCA file, no SQL file. Delete the old files after migration.
Migration Example 3: IRRE Child Records
BEFORE (Classic with IRRE):
// Parent TCA with inline field
'slides' => [
'label' => 'Slides',
'config' => [
'type' => 'inline',
'foreign_table' => 'tx_myext_domain_model_slide',
'foreign_field' => 'parentid',
'foreign_table_field' => 'parenttable',
'maxitems' => 10,
'appearance' => [
'collapseAll' => true,
'levelLinksPosition' => 'both',
'useSortable' => true,
],
],
],
// Separate TCA file for tx_myext_domain_model_slide
// Separate SQL for tx_myext_domain_model_slide
AFTER (Content Blocks with inline Collection):
# ContentBlocks/ContentElements/slider/config.yaml
name: myvendor/slider
fields:
- identifier: slides
type: Collection
labelField: title
maxitems: 10
appearance:
collapseAll: true
levelLinksPosition: both
fields:
- identifier: title
type: Text
- identifier: image
type: File
maxitems: 1
allowed: common-image-types
- identifier: link
type: Link
Or with separate Record Type as child:
# ContentBlocks/RecordTypes/slide/config.yaml
name: myvendor/slide
table: tx_myext_domain_model_slide
labelField: title
fields:
- identifier: title
type: Text
- identifier: image
type: File
maxitems: 1
- identifier: link
type: Link
# ContentBlocks/ContentElements/slider/config.yaml
name: myvendor/slider
fields:
- identifier: slides
type: Collection
foreign_table: tx_myext_domain_model_slide
shareAcrossTables: true
shareAcrossFields: true
Data Migration Script
When migrating existing content, you may need to rename columns and update CType values:
<?php
// Classes/Command/MigrateToContentBlocksCommand.php
namespace MyVendor\MyExt\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
class MigrateToContentBlocksCommand extends Command
{
protected function configure(): void
{
$this->setDescription('Migrate classic content elements to Content Blocks');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('tt_content');
// Step 1: Update CType from old to new
$oldCType = 'myext_hero';
$newCType = 'myvendor_hero'; // Content Blocks generates: vendor_name
$updated = $connection->update(
'tt_content',
['CType' => $newCType],
['CType' => $oldCType]
);
$output->writeln("Updated $updated records from $oldCType to $newCType");
// Step 2: Rename columns if Content Blocks uses different names
// Only needed if you used prefixFields: false and identifiers differ
// Usually Content Blocks generates: myvendor_hero_fieldname
// Example: Rename tx_myext_subheadline to myvendor_hero_subheadline
// This requires ALTER TABLE - use Database Compare in Install Tool instead
return Command::SUCCESS;
}
}
Database Column Renaming
If Content Blocks generates different column names, use the Install Tool:
- Create the Content Block (new columns will be detected)
- Go to Admin Tools → Maintenance → Analyze Database Structure
- Add new columns (Content Blocks generated)
- Run migration script to copy data:
-- Copy data from old columns to new columns
UPDATE tt_content
SET myvendor_hero_subheadline = tx_myext_subheadline
WHERE CType = 'myvendor_hero' AND tx_myext_subheadline != '';
-- After verification, drop old columns via Install Tool
Keeping Same Column Names (Recommended)
To avoid data migration, configure Content Blocks to use the same column names:
name: myvendor/hero
prefixFields: false # No automatic prefixing
fields:
- identifier: tx_myext_subheadline # Use exact old column name
type: Text
- identifier: tx_myext_image
type: File
- identifier: tx_myext_link
type: Link
Or use prefixField: false per field:
name: myvendor/hero
prefixFields: true # Enable prefixing by default
fields:
- identifier: tx_myext_subheadline
type: Text
prefixField: false # Keep original column name
Migration Checklist
## Pre-Migration
- [ ] List all content elements and record types to migrate
- [ ] Document existing TCA column names
- [ ] Backup database
- [ ] Install friendsoftypo3/content-blocks
## For Each Content Type
- [ ] Create config.yaml with field mappings
- [ ] Create frontend.html template
- [ ] Create backend-preview.html (optional)
- [ ] Create labels.xlf translations
- [ ] Run cache:flush and extension:setup
## Data Migration
- [ ] Update CType values in database
- [ ] Rename columns if needed (or use prefixField: false)
- [ ] Verify data displays correctly
## Cleanup
- [ ] Remove old TCA files
- [ ] Remove ext_tables.sql (or remove migrated columns)
- [ ] Remove old TypoScript rendering config
- [ ] Remove old Fluid templates
- [ ] Update documentation
Files to Delete After Migration
| Classic File | Why Remove |
|---|---|
Configuration/TCA/*.php |
Replaced by config.yaml |
Configuration/TCA/Overrides/tt_content.php |
Replaced by config.yaml |
ext_tables.sql |
Auto-generated by Content Blocks |
Configuration/TypoScript/setup.typoscript (rendering part) |
Auto-registered |
Resources/Private/Templates/ContentElements/*.html |
Moved to ContentBlocks folder |
17. Reverting Content Blocks to Classic Extension Format
This section guides you through converting Content Blocks back to traditional TYPO3 extension format with separate TCA, SQL, and TypoScript files.
When to Revert
| Scenario | Recommendation |
|---|---|
| Content Blocks has breaking changes | ✅ Revert to classic for stability |
| Need TCA features not supported by Content Blocks | ✅ Revert for full TCA control |
| Team prefers traditional TYPO3 structure | ✅ Revert for familiarity |
| Complex Extbase domain models needed | ✅ Revert for full Extbase integration |
| Performance-critical applications | ⚠️ Consider revert (measure first) |
| Simple content elements working fine | ❌ Keep Content Blocks |
Revert Strategy
┌─────────────────────────────────────────────────────────────────┐
│ 1. ANALYZE │
│ └─ Document all config.yaml files and field mappings │
├─────────────────────────────────────────────────────────────────┤
│ 2. GENERATE │
│ └─ Create TCA, SQL, TypoScript from config.yaml │
├─────────────────────────────────────────────────────────────────┤
│ 3. MOVE TEMPLATES │
│ └─ Move Fluid templates to traditional locations │
├─────────────────────────────────────────────────────────────────┤
│ 4. MIGRATE DATA │
│ └─ Update CTypes, rename columns if needed │
├─────────────────────────────────────────────────────────────────┤
│ 5. REMOVE DEPENDENCY │
│ └─ Uninstall Content Blocks, delete ContentBlocks folder │
└─────────────────────────────────────────────────────────────────┘
Content Blocks to TCA Field Mapping (Reverse)
| Content Blocks Type | TCA type | TCA renderType | Additional Config |
|---|---|---|---|
Text |
input |
- | max => 255 |
Textarea |
text |
- | rows => 5 |
Textarea + enableRichtext |
text |
- | enableRichtext => true |
Email |
email |
- | - |
Link |
link |
- | - |
Number |
number |
- | format => 'integer' or 'decimal' |
DateTime |
datetime |
- | format => 'date' or 'datetime' |
Color |
color |
- | - |
Checkbox |
check |
- | - |
Radio |
radio |
- | items => [...] |
Slug |
slug |
- | generatorOptions => [...] |
Password |
password |
- | - |
Select |
select |
selectSingle |
items => [...] |
Select + multiple |
select |
selectMultipleSideBySide |
- |
File |
file |
- | allowed => '...' |
Relation |
group or select |
- | foreign_table => '...' |
Category |
category |
- | - |
Collection (inline) |
inline |
- | foreign_table => '...' |
Collection (with fields) |
inline |
- | foreign_table => auto-generated |
FlexForm |
flex |
- | ds => [...] |
Json |
json |
- | - |
Tab |
- | - | --div--;Label in showitem |
Palette |
- | - | --palette--;;name in showitem |
YAML Options to TCA Config Mapping
| Content Blocks YAML | TCA Config Key | Example |
|---|---|---|
required: true |
required => true |
- |
default: "value" |
default => 'value' |
- |
placeholder: "text" |
placeholder => 'text' |
- |
minitems: 1 |
minitems => 1 |
- |
maxitems: 10 |
maxitems => 10 |
- |
allowed: common-image-types |
allowed => 'common-image-types' |
- |
displayCond: 'FIELD:x:=:1' |
displayCond => 'FIELD:x:=:1' |
- |
onChange: reload |
onChange => 'reload' |
- |
labelField: name |
ctrl['label'] => 'name' |
Record Types |
languageAware: true |
ctrl['languageField'] etc. |
Record Types |
sortable: true |
ctrl['sortby'] => 'sorting' |
Record Types |
softDelete: true |
ctrl['delete'] => 'deleted' |
Record Types |
Revert Example 1: Content Element
BEFORE (Content Blocks):
# ContentBlocks/ContentElements/hero/config.yaml
name: myvendor/hero
basics:
- TYPO3/Appearance
- TYPO3/Links
fields:
- identifier: header
useExistingField: true
- identifier: subheadline
type: Text
- identifier: hero_image
type: File
maxitems: 1
allowed: common-image-types
- identifier: cta_link
type: Link
- identifier: cta_text
type: Text
AFTER (Classic):
Step 1: Create TCA Override
<?php
// Configuration/TCA/Overrides/tt_content.php
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
defined('TYPO3') or die();
// Register the content element
ExtensionManagementUtility::addPlugin(
[
'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:ce.hero.title',
'value' => 'myext_hero',
'icon' => 'EXT:my_ext/Resources/Public/Icons/ContentElements/hero.svg',
'group' => 'default',
'description' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:ce.hero.description',
],
'CType',
'my_ext'
);
// Define columns
$tempColumns = [
'tx_myext_subheadline' => [
'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:field.subheadline',
'config' => [
'type' => 'input',
'size' => 50,
'max' => 255,
],
],
'tx_myext_hero_image' => [
'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:field.hero_image',
'config' => [
'type' => 'file',
'ma
…(truncated)