From craft-workspace-webconsulting-skills
Guides creating Content Elements, Record Types, Page Types, and File Types using TYPO3 Content Blocks extension—the single source of truth for YAML-based content modeling in TYPO3 14.x.
npx claudepluginhub dirnbauer/webconsulting-skillsThis skill uses the workspace's default tool permissions.
> **Compatibility:** This skill targets **TYPO3 v14.x** with **Content Blocks 2.x**. Always match the [Packagist `friendsoftypo3/content-blocks`](https://packagist.org/packages/friendsoftypo3/content-blocks) constraint to your Core version.
Produces, audits, and overhauls TYPO3 Content Blocks content elements styled with shadcn/ui presets using Fluid templates, Tailwind v4 tokens, backend previews, and seed scripts.
Designs content type hierarchies, reusable parts, and field compositions for headless CMS using Type > Part > Field pattern. Covers composition vs inheritance and multi-channel reusability.
Guides Craft CMS 5 content modeling: sections, entry types, fields, Matrix blocks, relations, eager loading, project config, and multi-site propagation.
Share bugs, ideas, or general feedback.
Compatibility: This skill targets TYPO3 v14.x with Content Blocks 2.x. Always match the Packagist
friendsoftypo3/content-blocksconstraint 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; adjustcomposer.jsonif 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.
Content Blocks is the modern approach to creating custom content types in TYPO3. It eliminates redundancy by providing a single YAML configuration that generates:
| 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 |
# Install via Composer (DDEV recommended)
ddev composer require friendsoftypo3/content-blocks
# After installation, clear caches
ddev typo3 cache:flush
Version constraint: Content Blocks 1.x requires TYPO3 ≥ 13.4 (
typo3/cms-core: ^13.4in the package). TYPO3 13.1–13.3 do not satisfy that Composer constraint.
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]
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) |
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.yaml
# 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
List under basics: to pull in Core field groups: TYPO3/Header, TYPO3/Appearance, TYPO3/Links, TYPO3/Categories. See the Content Blocks basics reference.
# EXT:my_sitepackage/ContentBlocks/ContentElements/hero-banner/config.yaml
name: myvendor/hero-banner
fields:
- identifier: header
useExistingField: true
- identifier: bodytext
useExistingField: true
# 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
<!-- 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>
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:
fixture.json, labels, and editor workflows unless the content model itself is changing.<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.f:asset.css / f:asset.script only for block-specific assets. Shared shadcn/Tailwind tokens and utility classes belong in the site CSS entrypoint.data-state attributes instead of React/Radix runtime dependencies.<!-- 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>
Record Types create custom database tables for structured data like teams, products, events, etc.
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!
# 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
# 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: Link
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 |
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: DateTime
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
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.
| 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 |
# EXT:my_sitepackage/ContentBlocks/PageTypes/blog-article/config.yaml
name: myvendor/blog-article
typeName: 1705234567
fields:
- identifier: author_name
type: Text
# 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)
| 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)
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.yaml
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>
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>
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.
To hide your page type from the "Create new page" drag area:
# Configuration/user.tsconfig
options {
pageTree {
doktypesToShowInNewPageDragArea := removeFromList(1705234567)
}
}
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.
| 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 |
# EXT:my_sitepackage/ContentBlocks/FileTypes/image-extended/config.yaml
name: myvendor/image-extended
typeName: image
fields:
- identifier: photographer
type: Text
label: Photographer
# 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: true
| 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 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 |
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>
| 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 |
| 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 |
| 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 |
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
TCA-only options: Keys like
appearance/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: 10
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
Important: Collections cannot be nested — a Collection field must NOT contain another Collection as a sub-field. See "Common Pitfalls" section for details.
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
Content Blocks automatically prefixes field identifiers to avoid collisions.
# 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
fields:
- identifier: my_custom_field
type: Text
prefixField: false # This field won't be prefixed
<!-- 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>
<!-- 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"/>
<!-- 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"/>
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
# 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
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 projects
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
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_mysitepackage
Use Extbase-compatible table names for Record Types:
table: tx_myextension_domain_model_myrecord
Reuse existing fields when possible:
- identifier: header
useExistingField: true
Group related fields with Tabs and Palettes:
- identifier: settings_tab
type: Tab
label: Settings
Use meaningful identifiers (snake_case):
- identifier: hero_background_image
Clear caches after changes:
ddev typo3 cache:flush -g system
ddev typo3 extension:setup --extension=my_sitepackage
Use labels.xlf for all user-facing labels
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_teammember
Don't use dashes in identifiers:
# ❌ Wrong
identifier: hero-image
# ✅ Correct
identifier: hero_image
Don't forget shareAcross options when using foreign_table in multiple places
# Clear all caches
ddev typo3 cache:flush
# Rebuild class loading
ddev composer dump-autoload
# Check extension setup
ddev typo3 extension:setup --extension=my_sitepackage
# 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 Structure
Use TYPO3 v14.1+ with Content Blocks 2.x (friendsoftypo3/content-blocks currently requires typo3/cms-core: ^14.1 — confirm on Packagist).
// ext_emconf.php — TYPO3 v14.1+ + Content Blocks 2.x
'depends' => [
'typo3' => '14.1.0-14.99.99',
'content_blocks' => '2.0.0-2.99.99',
],
For migration between classic TYPO3 extensions and Content Blocks, see the dedicated migration skill:
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 skills
Required 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)The following changes affect Content Blocks development on TYPO3 v14 only.
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.
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.
ctrl properties (#108027) — title, label, and other ctrl properties can now be overridden per record type in the types section.Content Block Fluid templates must comply with Fluid 5.x strict typing:
<f:argument> declarations._myVar).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.
These rules come from real debugging sessions. Violating them causes errors that are difficult to trace.
Content Blocks does NOT support a Collection inside another Collection. This causes a fatal Undefined array key "typeName" error in ProcessingInput.php during TYPO3 bootstrap — preventing composer install, cache:flush, and all CLI commands.
# BROKEN — nested Collection crashes content-blocks
fields:
- identifier: groups
type: Collection
fields:
- identifier: title
type: Text
- identifier: items # <-- Collection inside Collection = FATAL
type: Collection
fields:
- identifier: label
type: Text
- identifier: link
type: Link
# FIXED — flatten the inner Collection to a Textarea
fields:
- identifier: groups
type: Collection
fields:
- identifier: title
type: Text
- identifier: items # <-- Textarea with one entry per line
type: Textarea
label: Items (one per line)
rows: 5
The error message (Undefined array key "typeName" on table pages, type record-type) is misleading — it does not point to the actual nested Collection that causes it. If you see this error, grep for nested Collections: type: Collection inside another type: Collection.
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_myblock
The 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.
descriptionThe 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: Textarea
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.
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.
# DANGEROUS — "pages" collides with TYPO3 core table
- identifier: pages
type: Collection
# SAFE — use a descriptive, unique identifier
- identifier: page_items
type: Collection
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