
Typo3 Content Blocks
- 116 installs
- 33 repo stars
- Updated July 27, 2026
- dirnbauer/webconsulting-skills
Define TYPO3 Content Blocks for editor-friendly page elements when building CMS sites that need reusable, typed content components.
About
Teaches TYPO3 Content Blocks end to end: schema design, backend registration, Fluid output, and editor workflows so PHP CMS projects ship reusable structured content instead of brittle templates.
- Content Block schemas
- TCA and fieldsets
- Fluid rendering
- editor UX patterns
- reusable page elements
Typo3 Content Blocks by the numbers
- 116 all-time installs (skills.sh)
- Ranked #43 of 65 PHP & Laravel skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dirnbauer/webconsulting-skills --skill typo3-content-blocksAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 116 |
|---|---|
| repo stars | ★ 33 |
| Last updated | July 27, 2026 |
| Repository | dirnbauer/webconsulting-skills ↗ |
What it does
Define TYPO3 Content Blocks for editor-friendly page elements when building CMS sites that need reusable, typed content components.
Files
TYPO3 Content Blocks Development
Source: https://github.com/dirnbauer/webconsulting-skills
Compatibility: This skill targets TYPO3 v14.3+ with Content Blocks 2.x. Always match the Packagist `friendsoftypo3/content-blocks` constraint to your Core version.
For Content Blocks 1.x on TYPO3 v13, upstream requires TYPO3 ≥ 13.4 (typo3/cms-core: ^13.4) — confirm on Packagist.Examples use TYPO3 v14 APIs and CB 2.x; adjust composer.json if upstream constraints differ.TYPO3 API First: Always use TYPO3's built-in APIs, core features, and established conventions before creating custom implementations. Do not reinvent what TYPO3 already provides. Always verify that the APIs and methods you use exist and are not deprecated in TYPO3 v14 by checking the official TYPO3 documentation.
Migration Coverage: Content Blocks migration and cross-skill handoff guidance are documented directly in this skill and its local add-ons.
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:flushVersion constraint: Content Blocks 1.x requires TYPO3 ≥ 13.4 (typo3/cms-core: ^13.4 in the package). TYPO3 13.1–13.3 do not satisfy that Composer constraint.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 | Table | Use Case |
|---|---|---|---|
ContentElements | ContentBlocks/ContentElements/ | tt_content | Frontend content (hero, accordion, CTA) |
RecordTypes | ContentBlocks/RecordTypes/ | Custom/existing | Structured records (news, products, team) |
PageTypes | ContentBlocks/PageTypes/ | pages | Custom page types (blog, landing page) |
FileTypes | ContentBlocks/FileTypes/ | sys_file_reference | Extended file references (photographer, copyright) |
4. Folder Structure
EXT:my_sitepackage/
└── ContentBlocks/
├── ContentElements/
│ └── my-hero/
│ ├── assets/
│ │ └── icon.svg
│ ├── language/
│ │ └── labels.xlf
│ ├── templates/
│ │ ├── backend-preview.fluid.html
│ │ ├── frontend.fluid.html
│ │ └── partials/
│ └── config.yaml
├── RecordTypes/
│ └── my-record/
│ ├── assets/
│ │ └── icon.svg
│ ├── language/
│ │ └── labels.xlf
│ └── config.yaml
├── PageTypes/
│ └── blog-article/
│ ├── assets/
│ │ ├── icon.svg
│ │ ├── icon-hide-in-menu.svg
│ │ └── icon-root.svg
│ ├── language/
│ │ └── labels.xlf
│ ├── templates/
│ │ └── backend-preview.fluid.html
│ └── config.yaml
└── FileTypes/
└── image-extended/
├── language/
│ └── labels.xlf
└── config.yaml5. 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_sitepackagePredefined Basics (content elements)
List under basics: to pull in Core field groups: `TYPO3/Header`, `TYPO3/Appearance`, `TYPO3/Links`, `TYPO3/Categories`. See the Content Blocks basics reference.
Minimal Content Element
# EXT:my_sitepackage/ContentBlocks/ContentElements/hero-banner/config.yaml
name: myvendor/hero-banner
fields:
- identifier: header
useExistingField: true
- identifier: bodytext
useExistingField: trueFull 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 TextFrontend Template
<!-- templates/frontend.fluid.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>shadcn/ui Frontend Template Pattern
When a Content Block should follow shadcn/ui styling, keep the Content Block YAML as the content model and move repeated visual structure into shared Fluid components. The frontend.fluid.html entrypoint should map {data} fields to typed atomic components rather than duplicating bespoke CSS in every block.
<!-- templates/frontend.fluid.html -->
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
xmlns:d="http://typo3.org/ns/Vendor/Sitepackage/Components/ComponentCollection"
data-namespace-typo3-fluid="true">
<section class="py-12 md:py-16">
<d:molecule.card class="mx-auto max-w-2xl">
<d:molecule.cardHeader>
<d:molecule.cardTitle>{data.header}</d:molecule.cardTitle>
<f:if condition="{data.subheadline}">
<d:molecule.cardDescription>{data.subheadline}</d:molecule.cardDescription>
</f:if>
</d:molecule.cardHeader>
<d:molecule.cardContent>
<f:format.html>{data.bodytext}</f:format.html>
</d:molecule.cardContent>
</d:molecule.card>
</section>
</html>Guidelines:
- Preserve existing field identifiers,
fixture.json, labels, and editor workflows unless the content model itself is changing. - Use shared Fluid components for shadcn primitives such as Button, Badge, Card, Form controls, Tabs, Accordion, Alert, Table, and Sheet/Dialog shells.
- Put
<f:argument>type contracts in reusable Fluid components and partials. Avoid adding required arguments to a Content Block entry template unless the render context is fully controlled. - Use
f:asset.css/f:asset.scriptonly for block-specific assets. Shared shadcn/Tailwind tokens and utility classes belong in the site CSS entrypoint. - For interactive shadcn patterns, use Alpine or small vanilla controllers with ARIA and
data-stateattributes instead of React/Radix runtime dependencies. - Create or update styleguide fixtures so every Content Block can be rendered with realistic content in light and dark mode.
Backend Preview Template
<!-- templates/backend-preview.fluid.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>Detailed Reference
Read the full guide when the task needs detailed examples, long templates, troubleshooting matrices, appendices, or sections not included above. Keep this file unloaded for narrow tasks so the skill follows progressive disclosure.
6. Creating Record Types (Custom Tables)
Continues typo3-content-blocks from full guide.
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: TextFull 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
userGroup: true # fe_group visibility fields when applicable
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
renderType: selectSingle
items:
- label: LinkedIn
value: linkedin
- label: Twitter/X
value: twitter
- label: GitHub
value: github
- identifier: url
type: LinkRecord Type options (reference)
Content Blocks exposes many optional root keys on record types — always confirm names against the current Record Types YAML reference. Commonly used flags include:
| Option | Role |
|---|---|
editLocking | Editor locking behaviour |
sortField / sortable | Manual sorting (sorting column) |
rootLevelType | Allow records at PID 0 |
readOnly | Read-only in FormEngine |
adminOnly | Visible to admins only |
hideAtCopy / appendLabelAtCopy | Copy behaviour |
group | Backend selector grouping |
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: 0 # Integer ordering; higher values load first (higher priority)
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: DateTimeRecord 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: 17. Creating Page Types (Custom doktypes)
Continues typo3-content-blocks from full guide.
7. Creating Page Types (Custom doktypes)
Page Types extend the pages table with custom page types – ideal for blog articles, landing pages, news pages, or other page variants with special properties.
When to Use Page Types
| Use Case | Example |
|---|---|
| Structured page properties | Blog with author, teaser image, publish date |
| Plugin integration | News lists, event calendars reading page properties |
| Different page behavior | Landing pages without navigation |
| SEO-specific fields | Custom meta fields per page type |
Minimal Page Type
# EXT:my_sitepackage/ContentBlocks/PageTypes/blog-article/config.yaml
name: myvendor/blog-article
typeName: 1705234567
fields:
- identifier: author_name
type: TextFull Page Type Example
# EXT:my_sitepackage/ContentBlocks/PageTypes/blog-article/config.yaml
name: myvendor/blog-article
typeName: 1705234567 # Unix timestamp (unique identifier)
group: default # Options: default, link, special
fields:
- identifier: author_name
type: Text
label: Author
required: true
- identifier: teaser_text
type: Textarea
label: Teaser
- identifier: hero_image
type: File
allowed: common-image-types
maxitems: 1
- identifier: publish_date
type: DateTime
label: Publish Date
- identifier: reading_time
type: Number
label: Reading Time (minutes)Page Type Options
| Option | Type | Required | Description |
|---|---|---|---|
typeName | integer | ✓ | Unique doktype number (use Unix timestamp) |
group | string | Group in selector: default, link, special | |
allowedRecordTypes | array | Record types allowed on this doktype (default includes pages, sys_category, sys_file_reference, sys_file_collection; * wildcard possible — see official Page Types API) |
Reserved typeName values: 199, 254 (cannot be used)
Icons for Page States
Page Types support state-specific icons. Add these to your assets folder:
ContentBlocks/PageTypes/blog-article/
├── assets/
│ ├── icon.svg # Default icon
│ ├── icon-hide-in-menu.svg # Hidden in menu state
│ └── icon-root.svg # Site root state
└── config.yamlBackend Preview
Create a backend-preview.fluid.html to preview custom page properties:
<!-- templates/backend-preview.fluid.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="card card-size-medium">
<div class="card-body">
<be:link.editRecord uid="{data.uid}" table="{data.mainType}" fields="author_name">
<strong>Author:</strong> {data.author_name}
</be:link.editRecord>
<f:if condition="{data.publish_date}">
<br/><small>Published: <f:format.date format="d.m.Y">{data.publish_date}</f:format.date></small>
</f:if>
</div>
</div>
</html>Frontend Integration
Page Types have no automatic frontend rendering. Add the ContentBlocksDataProcessor to your TypoScript:
# Configuration/TypoScript/setup.typoscript
page = PAGE
page {
10 = FLUIDTEMPLATE
10 {
templateName = Default
templateRootPaths.10 = EXT:my_sitepackage/Resources/Private/Templates/
dataProcessing {
# Process Content Blocks page data
1 = content-blocks
}
}
}Then access fields in your Fluid template:
<!-- Resources/Private/Templates/Default.html -->
<f:if condition="{data.author_name}">
<p class="author">By {data.author_name}</p>
</f:if>
<f:if condition="{data.hero_image}">
<f:for each="{data.hero_image}" as="image">
<f:image image="{image}" class="hero-image"/>
</f:for>
</f:if>PAGEVIEW Content Areas and Columns [v14.2+ only]
For TYPO3 v14.2+ PAGEVIEW templates, prefer rendering backend layout columns through Core content areas instead of manually querying tt_content. This keeps Content Blocks, Fluid Styled Content, workspace overlays, language handling, and per-column context intact.
<main class="mx-auto w-full max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
<f:render.contentArea contentArea="{content.main}" />
</main>
<aside class="space-y-4">
<f:render.contentArea contentArea="{content.sidebar}" />
</aside>Use backend layout identifiers (main, sidebar, footer, etc.) as the page-template contract. Apply TYPO3 v14 content restrictions per column so wide hero/feature elements stay out of narrow sidebars, and pass the content area context to element templates when their rendering needs to change by column.
Remove from Page Tree Drag Area
To hide your page type from the "Create new page" drag area:
# Configuration/user.tsconfig
options {
pageTree {
doktypesToShowInNewPageDragArea := removeFromList(1705234567)
}
}8. Creating File Types (Extended Metadata)
Continues typo3-content-blocks from full guide.
8. Creating File Types (Extended Metadata)
New in version 1.2
File Types extend the sys_file_reference table with custom fields – perfect for photographer credits, copyright notices, or additional reference-level options.
Available File Type Names
| typeName | File Types |
|---|---|
image | JPEG, PNG, GIF, WebP, SVG |
video | MP4, WebM, OGG |
audio | MP3, WAV, OGG |
text | TXT, PDF, Markdown |
application | ZIP, Office formats |
Minimal File Type
# EXT:my_sitepackage/ContentBlocks/FileTypes/image-extended/config.yaml
name: myvendor/image-extended
typeName: image
fields:
- identifier: photographer
type: Text
label: PhotographerFull File Type Example
# EXT:my_sitepackage/ContentBlocks/FileTypes/image-extended/config.yaml
name: myvendor/image-extended
typeName: image
prefixFields: false # Keep original column names
fields:
- identifier: image_overlay_palette
type: Palette
label: 'LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:sys_file_reference.imageoverlayPalette'
fields:
# Reuse existing TYPO3 core fields
- identifier: alternative
useExistingField: true
- identifier: description
useExistingField: true
- type: Linebreak
- identifier: link
useExistingField: true
- identifier: title
useExistingField: true
- type: Linebreak
# Custom fields
- identifier: photographer
type: Text
label: Photographer
- identifier: copyright
type: Text
label: Copyright Notice
- identifier: source_url
type: Link
label: Source URL
- type: Linebreak
- identifier: crop
useExistingField: trueFile Type Options
| Option | Type | Required | Description |
|---|---|---|---|
typeName | string | ✓ | One of: text, image, audio, video, application |
prefixFields | boolean | Whether to prefix field identifiers with the Content Block name (often false for File Types to keep shared field names) |
Use Cases for File Types
| Use Case | Fields to Add |
|---|---|
| Photography agency | photographer, copyright, license_type, expiry_date |
| Video platform | director, duration, transcript, subtitles |
| Document management | document_version, author, confidentiality |
| E-commerce | product_sku, variant_color, variant_size |
Accessing File Type Fields
In Fluid templates, access custom metadata through FAL references:
<f:for each="{data.images}" as="image">
<figure>
<f:image image="{image}" alt="{image.alternative}"/>
<f:if condition="{image.properties.photographer}">
<figcaption>
Photo: {image.properties.photographer}
<f:if condition="{image.properties.copyright}">
| © {image.properties.copyright}
</f:if>
</figcaption>
</f:if>
</figure>
</f:for>9. Field Types Reference
Continues typo3-content-blocks from full guide.
9. 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 (+ format: integer or format: decimal as needed; legacy YAML sometimes used a non-existent Integer type — use 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 |
Basic | Shared “basic” field helper | type: Basic |
Country | Country selection (aligns with TCA country where supported) | type: Country |
Pass | Virtual field, not visible in the backend; used for storing data handled by extension logic | type: Pass |
SelectNumber | Select with numeric values | type: SelectNumber |
Uuid | UUID string | type: Uuid |
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 changeFile Field Example
TCA-only options: Keys likeappearance/behaviourbelong in generated TCA, not always in Content Blocks YAML. If the schema rejects them, add a TCA override for that field after generation (see Content Blocks docs on extending TCA).
fields:
- identifier: gallery_images
type: File
allowed: common-image-types
minitems: 1
maxitems: 10Select 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: compactCollection Field Example (Inline IRRE)
Collections can contain normal child fields and, on current Content Blocks, another Collection field for multilevel IRRE structures. Keep each level explicit with its own table, labelField, and item limits.
fields:
- identifier: accordion_items
type: Collection
prefixField: true
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 OpenMultilevel Collection Example
Use a multilevel Collection when editors need repeatable groups where each group has its own repeatable children. This Desiderio pattern models pricing plans, and each plan owns a nested list of features:
name: desiderio/pricing
typeName: desiderio_pricing
prefixFields: false
fields:
- identifier: plans
type: Collection
table: pricing_plans
prefixField: true
label: Pricing Plans
labelField: name
minitems: 1
maxitems: 4
fields:
- identifier: name
type: Textarea
rows: 1
required: true
- identifier: price
type: Textarea
rows: 1
required: true
- identifier: features
type: Collection
table: pricing_plan_features
label: Features
labelField: text
minitems: 1
maxitems: 8
fields:
- identifier: text
type: Textarea
rows: 1Guidelines for multilevel Collections:
- Use
prefixField: trueon the top-level Collection when the root Content Block hasprefixFields: falseor when identifiers likeitems,plans, orrowsare reused acrosstt_contenttypes. - Give every Collection level a stable
tablename. In the example,planswrites topricing_plans, and nestedfeatureswrites topricing_plan_features. - Keep nested levels shallow unless the editor workflow really needs them. Two levels is usually understandable; deeper structures become hard to seed, translate, preview, and migrate.
- In import/seed code, recurse through nested
Collectiondefinitions and write child rows using the current parent row uid, not the originaltt_contentuid. - If you use
foreign_table, do not also define localfieldsfor that Collection; define the reusable structure as a Record Type and reference it instead.
10. Field Prefixing
Continues typo3-content-blocks from full guide.
10. 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: falseDisable Prefixing per Field
fields:
- identifier: my_custom_field
type: Text
prefixField: false # This field won't be prefixedPrefix Collection Fields When Root Fields Are Shared
If a tt_content Content Block uses prefixFields: false, still enable prefixing on every top-level Collection field that could share an identifier with another Content Block, such as items, links, plans, or members.
name: vendor/accordion
typeName: vendor_accordion
prefixFields: false
fields:
- identifier: items
type: Collection
table: accordion_items
prefixField: true
fields:
- identifier: title
type: Textarea
- identifier: content
type: Textarea
enableRichtext: trueDo not work around reused Collection identifiers by patching columnsOverrides or rewriting foreign_table per CType. In TYPO3 TCA there is one root column per field identifier; without prefixField: true, the last merged Collection configuration can win and point multiple content elements at the wrong child table.
Reusing Collection Tables to Reduce Schema Noise
Default to one generated child table per Collection. The many-table setup is safer and clearer for generated Content Blocks because each content element owns its schema, labels, migrations, fixtures, and seed logic.
Reuse a Collection table only as an explicit modeling decision, not automatically by shared identifiers like items, links, plans, or members.
Use table reuse when all of these are true:
- The child schema is intentionally identical, for example repeated
label+link,text,label+value, or logo rows. - The shared table name describes a real reusable concept, not just a coincidental field identifier.
- Every parent Collection that points to the table is configured consistently.
- If the same parent record can have multiple fields using the same child table, use
foreign_tablewithshareAcrossFields: trueor another explicit match field so rows cannot leak between fields. - Import, seed, cleanup, and migration code can resolve the shared table and match fields correctly.
Avoid table reuse when:
- The field schemas differ or may evolve independently.
- The only reason is to reduce a visually large table list.
- Two fields on the same content element would point to the same child table without
shareAcrossFields. - Editors need different labels, validation, record labels, or preview assumptions per content element.
- You would need nullable catch-all columns to force unrelated structures into one table.
Expected benefit: fewer tables and less schema noise. Do not expect a major physical database-size reduction unless table overhead is the actual bottleneck in the target database. Reuse tables for small, stable primitives; keep bespoke content structures isolated.
11. Templating Features
Continues typo3-content-blocks from full guide.
11. 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"/>12. Extending Existing Tables
Continues typo3-content-blocks from full guide.
12. 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: Text13. Workflow with DDEV
Continues typo3-content-blocks from full guide.
13. 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 StructureUsing webprofil/make Extension
If webprofil/make is installed:
# Create Content Block with webprofil/make
ddev make:content_blocks
# Clear caches and update database (prefer Core CLI)
ddev typo3 cache:flush
ddev typo3 extension:setup --extension=my_sitepackage
# `database:updateschema` exists only with helhum/typo3-console — do not assume it in plain Core projectsIntegration 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_sitepackage14. Defaults Configuration
Continues typo3-content-blocks from full guide.
14. 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/Header
- TYPO3/Appearance
- TYPO3/Links
- TYPO3/Categories
group: default
prefixFields: true
prefixType: full
record-type:
prefixFields: true
prefixType: vendor
vendorPrefix: tx_mysitepackage15. Best Practices
Continues typo3-content-blocks from full guide.
15. Best Practices
DO ✅
1. Use Extbase-compatible table names for Record Types:
table: tx_myextension_domain_model_myrecord2. Reuse existing fields when possible:
- identifier: header
useExistingField: true3. Group related fields with Tabs and Palettes:
- identifier: settings_tab
type: Tab
label: Settings4. Use meaningful identifiers (snake_case):
- identifier: hero_background_image5. Clear caches after changes:
ddev typo3 cache:flush -g system
ddev typo3 extension:setup --extension=my_sitepackage6. Use labels.xlf for all user-facing labels
DON'T ❌
1. Don't use raw SQL - Content Blocks generates schema automatically
2. Don't duplicate TCA - Config.yaml is the single source of truth
3. Don't use short table names for Extbase integration:
# ❌ Wrong
table: team_member
# ✅ Correct
table: tx_mysitepackage_domain_model_teammember4. Don't use dashes in identifiers:
# ❌ Wrong
identifier: hero-image
# ✅ Correct
identifier: hero_image5. Don't forget shareAcross options when using foreign_table in multiple places
16. Troubleshooting
Continues typo3-content-blocks from full guide.
16. 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_sitepackageDatabase Errors
# Update database schema (Core)
ddev typo3 extension:setup --extension=my_sitepackage
# With typo3-console only: ddev typo3 database:updateschema
# Or use Compare Tool
# Admin Tools > Maintenance > Analyze Database StructureField 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
17. Version constraints
Continues typo3-content-blocks from full guide.
17. Version constraints
Use TYPO3 v14.3+ with Content Blocks 2.x (friendsoftypo3/content-blocks 2.3.x/2.4.x — currently 2.4.5 — requires typo3/cms-core: ^14.3 — confirm on Packagist).
// ext_emconf.php — TYPO3 v14.3+ + Content Blocks 2.x
'depends' => [
'typo3' => '14.3.0-14.99.99',
'content_blocks' => '2.0.0-2.99.99',
],---
18. Package Management for v14 Compatibility
Continues typo3-content-blocks from full guide.
18. Package Management for v14 Compatibility
If extensions don't support TYPO3 v14 yet, fork and update:
# Add private fork as VCS repository in composer.json
# Then update extension for v14 using typo3-rector and typo3-update skillsRequired private forks for webconsulting stack:
webprofil/wp-t3monitoring-client(^13 → ^14)webprofil/deployer(^13 → ^14)webprofil/crawler(^1.0 → compatible with v14)webprofil/make(^1.1 → compatible with v14)
---
Common Pitfalls & Hard-Won Lessons
Continues typo3-content-blocks from full guide.
Common Pitfalls & Hard-Won Lessons
These rules come from real debugging sessions. Violating them causes errors that are difficult to trace.
1. Multilevel Collections Need Explicit Tables
Current Content Blocks supports a Collection inside another Collection. Use it intentionally for structures like pricing plans with feature rows, table rows with cells, or navigation groups with links.
# GOOD - each level has its own table and label field
fields:
- identifier: rows
type: Collection
table: data_table_rows
prefixField: true
labelField: row_label
fields:
- identifier: row_label
type: Textarea
- identifier: cells
type: Collection
table: data_table_cells
labelField: value
fields:
- identifier: value
type: TextareaAvoid anonymous or ambiguous multilevel structures. If a nested set is reused across multiple parent fields or parent tables, extract it to a Record Type and reference it with foreign_table, plus shareAcrossTables / shareAcrossFields where needed.
2. Dashes in typeName Values
When Content Blocks auto-generates typeName from the name field, it strips all dashes. If you set typeName explicitly, it must follow the same convention — no dashes.
# WRONG — dash in typeName causes CType mismatch
name: myvendor/my-block
typeName: myvendor_my-block
# CORRECT — no dashes, matches auto-generation
name: myvendor/my-block
typeName: myvendor_myblockThe auto-generation logic (UniqueIdentifierCreator::removeDashes) converts myvendor/my-block to myvendor_myblock. If your explicit typeName uses dashes, it won't match existing database records created by auto-generation.
3. Reserved Field Identifier: description
The identifier description is a top-level config.yaml key (the content block's description shown in the backend). Using it as a field identifier creates a type conflict — the config key is a string, but a Textarea field resolves to a different type.
# WRONG — conflicts with the config.yaml root key
description: My content block description
fields:
- identifier: description # <-- conflicts with root "description"
type: Textarea
# CORRECT — use a distinct identifier
description: My content block description
fields:
- identifier: description_text # <-- no conflict
type: Textarea4. Template Field References Must Match config.yaml Identifiers
Every {data.fieldname} in frontend.html must exactly match an identifier in config.yaml. There is no runtime error — the field simply renders empty, making this a silent bug.
# config.yaml defines:
- identifier: features_list
type: Textarea<!-- WRONG — silent failure, renders empty -->
{data.features -> f:split(separator: '\n')}
<!-- CORRECT — matches the identifier -->
{data.features_list -> f:split(separator: '\n')}When renaming field identifiers (e.g., to resolve conflicts), always search templates for the old name.
5. Collection Identifier Naming — Avoid Table Name Collisions
Never name a Collection field with an identifier that matches an existing TYPO3 database table (e.g., pages, tt_content, sys_file). Content Blocks generates table names from Collection identifiers, and a collision with a core table causes unpredictable errors.
For tt_content Content Blocks with prefixFields: false, do not reuse an unprefixed top-level Collection identifier across multiple content elements. Either give each Collection a unique identifier or set prefixField: true on that Collection so Content Blocks generates a separate TCA column for each content element.
# DANGEROUS — "pages" collides with TYPO3 core table
- identifier: pages
type: Collection
# SAFE — use a descriptive, unique identifier
- identifier: page_items
type: Collection---
Credits & Attribution
Continues typo3-content-blocks from full guide.
Credits & Attribution
This skill incorporates information from the official Content Blocks documentation maintained by the TYPO3 Content Types Team and Friends of TYPO3.
Original documentation: https://docs.typo3.org/p/friendsoftypo3/content-blocks/
Adapted by webconsulting.at for this skill collection
Source: https://github.com/dirnbauer/webconsulting-skills
TYPO3 Content Blocks Development Full Guide
Read only the section that matches the current task. These files continue the main SKILL.md after its lightweight workflow and examples.
Sections
- 6. Creating Record Types (Custom Tables)
- 7. Creating Page Types (Custom doktypes)
- 8. Creating File Types (Extended Metadata)
- 9. Field Types Reference
- 10. Field Prefixing
- 11. Templating Features
- 12. Extending Existing Tables
- 13. Workflow with DDEV
- 14. Defaults Configuration
- 15. Best Practices
- 16. Troubleshooting
- 17. Version constraints
- Related Skills
- 18. Package Management for v14 Compatibility
- References
- v14-Only Changes
- Common Pitfalls & Hard-Won Lessons
- Credits & Attribution
References
Continues typo3-content-blocks from full guide.
References
- Content Blocks Documentation
- YAML Reference
- Field Types
- Content Elements API
- Record Types API
- Page Types API
- File Types YAML Reference
- Migration Skill
- TYPO3 shadcn content elements
- Packagist: friendsoftypo3/content-blocks
Related Skills
Continues typo3-content-blocks from full guide.
Related Skills
For migration between classic TYPO3 extensions and Content Blocks, see the dedicated migration skill:
- [typo3-content-blocks-migration](../SKILL-MIGRATION.md) - Bidirectional migration guide with:
- TCA → Content Blocks field mapping
- Content Blocks → TCA reverse mapping
- Data migration scripts
- Step-by-step examples
- Checklists for both directions
---
v14-Only Changes
Continues typo3-content-blocks from full guide.
v14-Only Changes
The following changes affect Content Blocks development on TYPO3 v14 only.
New TCA Type country [v14 only]
TYPO3 v14 introduces a native country TCA type (#99911). Content Blocks can use this for country selection fields. Check Content Blocks YAML documentation for support of this field type.
TCA itemsProcessors [v14 only]
New itemsProcessors option (#107889) enables dynamic item generation for select fields. Content Blocks may expose this via YAML configuration for advanced select field customization.
Type-Specific TCA Properties [v14 only]
- Type-specific `ctrl` properties (#108027) —
title,label, and other ctrl properties can now be overridden per record type in thetypessection. - Type-specific TCA defaults (#107281) — default values can differ per record type.
Fluid 5.x / 5.3 Strict Types [v14 only]
Content Block Fluid templates must comply with Fluid 5.x strict typing:
- ViewHelper arguments are strictly typed. Ensure correct types (int vs string).
- Reusable components and partials should define their public API with root-level
<f:argument>declarations. - Union types are available for component arguments, but use them sparingly because each branch must be handled explicitly.
- No underscore-prefixed variable names (
_myVar). - CDATA sections are preserved (not stripped).
Content Element Restrictions per Column [v14.1+ only]
TYPO3 v14.1 integrates content_defender functionality into Core. Backend layouts can now restrict which Content Elements (including Content Blocks) are allowed per colPos without third-party extensions.
---
Content Blocks Migration Guide
Compatibility: TYPO3 v14.x
>
Related Skill: typo3-content-blocks - Main Content Blocks development guide
This skill covers bidirectional migration between classic TYPO3 extensions (TCA/SQL/TypoScript) and the modern Content Blocks approach.
---
1. 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 |
|---|---|---|---|
datetime | - | DateTime | Preferred standalone type (v12+) |
link | - | Link | Preferred standalone type |
color | - | Color | Preferred standalone type |
email | - | Email | Preferred standalone type |
number | - | Number | Preferred standalone type |
password | - | Password | Preferred standalone type |
slug | - | Slug | Preferred standalone type |
country | - | Country | Country selector |
uuid | - | Uuid | UUID string |
input | - | Text | Legacy/basic text (pre–standalone types) |
input | inputDateTime | DateTime | Legacy combo — migrate to type: datetime in TCA |
input | inputLink | Link | Legacy combo — migrate to type: link |
input | colorPicker | Color | Legacy combo — migrate to type: color |
input | slug | Slug | Legacy combo — migrate to type: 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 + renderType: selectMultipleSideBySide + maxitems: 2 (or higher) | Side-by-side multi-select |
select | selectCheckBox | Select + renderType: selectCheckBox | Checkbox group |
group | - | Relation | group always maps to Relation in v14 |
file | - | File | FAL references (standalone TCA type since v10) |
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):
ExtensionManagementUtility::addPlugin()for CType registration is deprecated since TYPO3 v12 — shown here for legacy codebases; new extensions should register viaConfiguration/TCA/Overrides/tt_content.phpitemsor Content Blocks.
// 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.fluid.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',
],
// hidden, starttime, endtime columns are auto-created from ctrl (TYPO3 v14)
'palettes' => [
'visibility' => ['showitem' => 'hidden'],
'access' => ['showitem' => 'starttime, endtime'],
],
'columns' => [
'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',
],
],
// ... more columns
],
'types' => [
'1' => [
'showitem' => '
name, price, description,
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
--palette--;;visibility,
--palette--;;access,
',
],
],
];-- 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
);AFTER (Content Blocks):
# ContentBlocks/RecordTypes/product/config.yaml
name: myvendor/product
table: tx_myext_domain_model_product
labelField: name
languageAware: true
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: price
type: Number
format: decimal
- identifier: description
type: Textarea
enableRichtext: trueThat's it! No TCA file, no SQL file.
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_slideAFTER (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: LinkOr 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: trueData 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");
return Command::SUCCESS;
}
}Database Column Renaming
If Content Blocks generates different column names, use the Install Tool:
1. Create the Content Block (new columns will be detected) 2. Go to Admin Tools → Maintenance → Analyze Database Structure 3. Add new columns (Content Blocks generated) 4. 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 ToolKeeping 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: LinkOr 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 nameMigration 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.fluid.html template
- [ ] Create backend-preview.fluid.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 documentationFiles 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 |
---
2. 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 | select | selectMultipleSideBySide | maxitems => 2 (or higher) for multi-select |
File | file | - | allowed => '...' |
Relation | group | - | allowed => 'tablename' |
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: 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: TextAFTER (Classic):
Step 1: Create TCA Override
<?php
// Configuration/TCA/Overrides/tt_content.php
defined('TYPO3') or die();
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
// Register CType in tt_content selector (avoid deprecated addPlugin for new code)
$GLOBALS['TCA']['tt_content']['columns']['CType']['config']['items'][] = [
'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',
];
// 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',
'maxitems' => 1,
'allowed' => 'common-image-types',
],
],
'tx_myext_cta_link' => [
'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:field.cta_link',
'config' => [
'type' => 'link',
],
],
'tx_myext_cta_text' => [
'label' => 'LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:field.cta_text',
'config' => [
'type' => 'input',
'size' => 30,
'max' => 100,
],
],
];
ExtensionManagementUtility::addTCAcolumns('tt_content', $tempColumns);
// Define showitem
$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_hero_image,
tx_myext_cta_link,
tx_myext_cta_text,
--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,
',
];Step 2: Create SQL Schema
-- ext_tables.sql
CREATE TABLE tt_content (
tx_myext_subheadline varchar(255) DEFAULT '' NOT NULL,
tx_myext_hero_image int(11) unsigned DEFAULT 0 NOT NULL,
tx_myext_cta_link varchar(1024) DEFAULT '' NOT NULL,
tx_myext_cta_text varchar(100) DEFAULT '' NOT NULL
);Step 3: Create TypoScript Rendering
# Configuration/TypoScript/setup.typoscript
tt_content.myext_hero = FLUIDTEMPLATE
tt_content.myext_hero {
templateName = Hero
templateRootPaths {
10 = EXT:my_ext/Resources/Private/Templates/ContentElements/
}
dataProcessing {
10 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
10 {
references.fieldName = tx_myext_hero_image
as = heroImages
}
}
}Step 4: Update Fluid Template
<!-- Resources/Private/Templates/ContentElements/Hero.html -->
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:layout name="Default"/>
<f:section name="Main">
<section class="hero-banner">
<f:if condition="{heroImages}">
<f:for each="{heroImages}" 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.tx_myext_subheadline}">
<p class="subheadline">{data.tx_myext_subheadline}</p>
</f:if>
<f:if condition="{data.tx_myext_cta_link}">
<f:link.typolink parameter="{data.tx_myext_cta_link}" class="btn btn-primary">
{data.tx_myext_cta_text -> f:or(default: 'Learn more')}
</f:link.typolink>
</f:if>
</div>
</section>
</f:section>
</html>Data Migration Script (Revert Direction)
<?php
// Classes/Command/RevertFromContentBlocksCommand.php
namespace MyVendor\MyExt\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
#[AsCommand(
name: 'myext:revert-content-blocks',
description: 'Revert Content Blocks elements to classic TCA format'
)]
class RevertFromContentBlocksCommand extends Command
{
private const CTYPE_MAPPING = [
'myvendor_hero' => 'myext_hero',
'myvendor_accordion' => 'myext_accordion',
];
private const COLUMN_MAPPING = [
'myvendor_hero_subheadline' => 'tx_myext_subheadline',
'myvendor_hero_hero_image' => 'tx_myext_hero_image',
];
protected function configure(): void
{
$this
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would be changed')
->addOption('copy-data', null, InputOption::VALUE_NONE, 'Copy column data');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$dryRun = $input->getOption('dry-run');
$copyData = $input->getOption('copy-data');
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
->getConnectionForTable('tt_content');
$io->title('Reverting Content Blocks to Classic Format');
// Update CTypes
foreach (self::CTYPE_MAPPING as $oldCType => $newCType) {
$count = $connection->count('*', 'tt_content', ['CType' => $oldCType]);
$io->writeln(" $oldCType → $newCType: $count records");
if (!$dryRun && $count > 0) {
$connection->update('tt_content', ['CType' => $newCType], ['CType' => $oldCType]);
}
}
// Copy column data if requested
if ($copyData && !$dryRun) {
foreach (self::COLUMN_MAPPING as $oldColumn => $newColumn) {
try {
$connection->executeStatement(
"UPDATE tt_content SET $newColumn = $oldColumn WHERE $oldColumn IS NOT NULL AND $oldColumn != ''"
);
} catch (\Exception $e) {
$io->warning("Column copy failed: " . $e->getMessage());
}
}
}
$io->success($dryRun ? 'Dry run completed' : 'Migration completed');
return Command::SUCCESS;
}
}Template Variable Changes
When reverting, update Fluid template variable names:
| Content Blocks | Classic |
|---|---|
{data.fieldname} | {data.tx_myext_fieldname} |
{data.my_image} → auto-resolved | Use DataProcessor + {processedImages} |
{data.collection_items} → auto-resolved | Use DatabaseQueryProcessor + {items} |
{cb:assetPath()} | Static path: EXT:my_ext/Resources/Public/... |
{cb:languagePath()} | LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf: |
Complete Revert Checklist
## Pre-Revert
- [ ] Document all Content Blocks config.yaml files
- [ ] Map field identifiers to TCA column names
- [ ] Backup database
- [ ] Create branch for revert work
## For Each Content Type
- [ ] Create TCA PHP file(s)
- [ ] Add columns to ext_tables.sql
- [ ] Create TypoScript rendering
- [ ] Move/update Fluid templates (update variable names!)
- [ ] Move translations to locallang.xlf
- [ ] Move icons to Resources/Public/Icons/
## Data Migration
- [ ] Update CType values in database
- [ ] Copy data from Content Blocks columns to classic columns
- [ ] Test data displays correctly
- [ ] Verify file relations work
## Cleanup
- [ ] Remove ContentBlocks folder
- [ ] Remove friendsoftypo3/content-blocks from composer.json
- [ ] Run: composer update
- [ ] Remove old columns via Install Tool (after verification)
- [ ] Clear all caches
## Testing
- [ ] All content elements render correctly
- [ ] All record types editable in backend
- [ ] File relations display correctly
- [ ] Translations work
- [ ] No PHP errors in logRemoving Content Blocks Dependency
# After successful revert and testing
ddev composer remove friendsoftypo3/content-blocks
# Clear caches
ddev typo3 cache:flush
# Update database (remove orphaned columns)
# Go to Admin Tools → Maintenance → Analyze Database Structure
# Select "Remove" for the old Content Blocks columns---
References
- Content Blocks Documentation
- TCA Reference
- Main Content Blocks Skill
---
Credits & Attribution
This skill is part of the webconsulting.at TYPO3 skills collection, adapted from the official Content Blocks documentation.