
Typo3 Typoscript Ref
- 43 installs
- 2 repo stars
- Updated August 3, 2026
- netresearch/typo3-typoscript-ref-skill
Helps with ai & agent building tasks.
About
typo3-typoscript-ref is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- typo3-typoscript-ref
- AI & Agent Building
- AI-coding skill
Typo3 Typoscript Ref by the numbers
- 43 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #7,921 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/netresearch/typo3-typoscript-ref-skill --skill typo3-typoscript-refAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 43 |
|---|---|
| repo stars | ★ 2 |
| Last updated | August 3, 2026 |
| Repository | netresearch/typo3-typoscript-ref-skill ↗ |
What it does
Helps with ai & agent building tasks.
Files
TYPO3 TypoScript, TSconfig and Fluid Reference
Version-aware local lookup with always-on best practices.
Usage
scripts/lookup.sh "stdWrap wrap" # Reference lookup
scripts/lookup.sh "PAGEVIEW" --with-fluid # With Fluid context
scripts/lookup.sh --recipe page-setup # Recipe for common tasks
scripts/lookup.sh "FLUIDTEMPLATE" --review # Adds deprecation warnings
scripts/lookup.sh --deprecations # Deprecation list
scripts/lookup.sh --checklist typoscript # Review checklist (typoscript|tsconfig|fluid)
scripts/lookup.sh --lint-rules # Project lint rules
scripts/lookup.sh --debug "The page is not configured" # Debug error
scripts/lookup.sh --update # Update cache
scripts/lookup.sh "TEXT" --version 12 # Override versionRules
1. ALWAYS run lookup.sh before writing or reviewing TypoScript/TSconfig/Fluid code 2. ALWAYS follow best practice annotations (required/deprecated/recommended/tip) 3. ALWAYS check project lint rules (--lint-rules) before writing TypoScript 4. When writing NEW code: use the most modern approach for the detected version 5. When reviewing EXISTING code: flag deprecated patterns, check --deprecations for the project's version 6. For combined TypoScript+Fluid tasks: use --with-fluid flag 7. Never generate config.no_cache = 1 in production setups 8. Prefer DataProcessors over CONTENT cObject in Fluid-based templates
Version-Specific Guidance
- v12: Use FLUIDTEMPLATE, sys_template static includes, constants.typoscript
- v13: Prefer PAGEVIEW for new page templates, introduce Site Sets, use settings.definitions.yaml
- v14: Site Sets mandatory, PAGEVIEW is the standard for page rendering (FLUIDTEMPLATE is legacy, not officially deprecated), @import mandatory (INCLUDE_TYPOSCRIPT removed), getTSFE() conditions removed, prefer PKG: over EXT: resource paths
When answering version-specific questions, always consult references/review/deprecations.md and the relevant migration guide (migration-v12-to-v13.md or migration-v13-to-v14.md).
Review Workflow
When reviewing TypoScript/TSconfig/Fluid code:
1. Run --checklist for the file type (typoscript, tsconfig, or fluid) 2. Run --deprecations filtered to project version 3. Cross-reference references/review/common-mistakes.md for known pitfalls 4. Check references/review/security.md for Fluid XSS patterns (f:format.raw, f:sanitize.html) 5. Check references/review/performance.md for COA_INT/USER_INT overuse 6. Use --review flag on keyword lookups to append deprecation context
Reference Index
| Need | Reference |
|---|---|
| TypoScript patterns, Fluid best practices | references/patterns.md |
| Debugging errors | references/debugging.md |
| Deprecation lists | references/review/deprecations.md |
| Security (XSS, escaping) | references/review/security.md |
| Performance (caching, INT objects) | references/review/performance.md |
| Common mistakes | references/review/common-mistakes.md |
| Migration v12-v13 | references/review/migration-v12-to-v13.md |
| Migration v13-v14 | references/review/migration-v13-to-v14.md |
| Topic index (lookup.sh) | references/topic-index.md |
First Run
scripts/lookup.sh --update[
{
"name": "lookup_stdwrap",
"prompt": "I need to use stdWrap wrap in my TYPO3 TypoScript setup. Look up the reference for me.",
"assertions": [
{ "type": "tool_use", "tool": "Bash", "pattern": "lookup\\.sh.*stdWrap" },
{ "type": "content", "pattern": "stdWrap|wrap" }
]
},
{
"name": "lookup_fluidtemplate",
"prompt": "How do I configure a FLUIDTEMPLATE content object in TYPO3 v12 TypoScript?",
"assertions": [
{ "type": "tool_use", "tool": "Bash", "pattern": "lookup\\.sh.*FLUIDTEMPLATE" },
{ "type": "content", "pattern": "templateRootPaths|templateName" },
{ "type": "content", "pattern": "(v12|version 12)" }
]
},
{
"name": "pageview_vs_fluidtemplate",
"prompt": "Should I use PAGEVIEW or FLUIDTEMPLATE for a new TYPO3 v13 project?",
"assertions": [
{ "type": "content", "pattern": "PAGEVIEW" },
{ "type": "content", "pattern": "(recommend|prefer|modern|new)" },
{ "type": "content", "pattern": "FLUIDTEMPLATE" }
]
},
{
"name": "recipe_page_setup",
"prompt": "Show me the recipe for a basic page setup in TypoScript.",
"assertions": [
{ "type": "tool_use", "tool": "Bash", "pattern": "lookup\\.sh.*--recipe.*page-setup" },
{ "type": "content", "pattern": "page\\s*=\\s*PAGE" }
]
},
{
"name": "recipe_menu_setup",
"prompt": "How do I create a navigation menu in TypoScript? Show me the recipe.",
"assertions": [
{ "type": "tool_use", "tool": "Bash", "pattern": "lookup\\.sh.*--recipe.*menu" },
{ "type": "content", "pattern": "(HMENU|MenuProcessor|menu)" }
]
},
{
"name": "deprecations_v13",
"prompt": "What TypoScript features are deprecated in TYPO3 v13?",
"assertions": [
{ "type": "tool_use", "tool": "Bash", "pattern": "lookup\\.sh.*--deprecations" },
{ "type": "content", "pattern": "(INCLUDE_TYPOSCRIPT|Site Sets|deprecated)" }
]
},
{
"name": "deprecations_v14",
"prompt": "What TypoScript changes are removed in TYPO3 v14?",
"assertions": [
{ "type": "content", "pattern": "INCLUDE_TYPOSCRIPT" },
{ "type": "content", "pattern": "(removed|removal)" }
]
},
{
"name": "debug_page_not_configured",
"prompt": "I get 'The page is not configured' error in my TYPO3 frontend. How do I fix it?",
"assertions": [
{ "type": "tool_use", "tool": "Bash", "pattern": "lookup\\.sh.*--debug.*page.*not configured" },
{ "type": "content", "pattern": "(PAGE|page\\s*=|typeNum)" }
]
},
{
"name": "debug_template_not_found",
"prompt": "TYPO3 shows 'No TypoScript template found' on my page. What is wrong?",
"assertions": [
{ "type": "tool_use", "tool": "Bash", "pattern": "lookup\\.sh.*--debug" },
{ "type": "content", "pattern": "(sys_template|Site Set|root)" }
]
},
{
"name": "checklist_typoscript",
"prompt": "Give me a code review checklist for TypoScript files.",
"assertions": [
{ "type": "tool_use", "tool": "Bash", "pattern": "lookup\\.sh.*--checklist.*typoscript" },
{ "type": "content", "pattern": "(no_cache|deprecated|stdWrap)" }
]
},
{
"name": "checklist_fluid",
"prompt": "What should I check when reviewing Fluid templates?",
"assertions": [
{ "type": "tool_use", "tool": "Bash", "pattern": "lookup\\.sh.*--checklist.*fluid" },
{ "type": "content", "pattern": "(f:translate|f:format\\.raw|escap)" }
]
},
{
"name": "review_existing_typoscript",
"prompt": "Review this TypoScript for issues:\n```typoscript\npage = PAGE\npage.10 = FLUIDTEMPLATE\npage.10.file = EXT:my_ext/template.html\nconfig.no_cache = 1\n[globalVar = GP:L=1]\n config.sys_language_uid = 1\n[end]\n```",
"assertions": [
{ "type": "content", "pattern": "(no_cache|deprecated|production)" },
{ "type": "content", "pattern": "(condition|globalVar|legacy|Symfony)" },
{ "type": "content", "pattern": "(templateRootPaths|file)" }
]
},
{
"name": "lint_rules_lookup",
"prompt": "What lint rules should I follow when writing TypoScript?",
"assertions": [
{ "type": "tool_use", "tool": "Bash", "pattern": "lookup\\.sh.*--lint" },
{ "type": "content", "pattern": "(indent|spacing|lint)" }
]
},
{
"name": "site_sets_v13",
"prompt": "How do I set up Site Sets in TYPO3 v13 to distribute my extension's TypoScript?",
"assertions": [
{ "type": "content", "pattern": "config\\.yaml" },
{ "type": "content", "pattern": "(settings\\.definitions|dependencies|Configuration/Sets)" }
]
},
{
"name": "coa_vs_coa_int",
"prompt": "When should I use COA_INT instead of COA in TypoScript?",
"assertions": [
{ "type": "content", "pattern": "COA_INT" },
{ "type": "content", "pattern": "(uncached|dynamic|per.request|every request)" },
{ "type": "content", "pattern": "(cache|performance|overhead)" }
]
},
{
"name": "copy_vs_reference",
"prompt": "What is the difference between < and =< operators in TypoScript?",
"assertions": [
{ "type": "content", "pattern": "(copy|independent)" },
{ "type": "content", "pattern": "(reference|linked|current state)" }
]
},
{
"name": "fluid_xss_prevention",
"prompt": "How do I prevent XSS in my Fluid templates?",
"assertions": [
{ "type": "content", "pattern": "(escape|auto.escap|f:format\\.raw)" },
{ "type": "content", "pattern": "(f:sanitize\\.html|trusted|user.input)" }
]
},
{
"name": "dataprocessor_usage",
"prompt": "I need to fetch news records and display them in a Fluid template. Should I use CONTENT cObject or DatabaseQueryProcessor?",
"assertions": [
{ "type": "content", "pattern": "DatabaseQueryProcessor" },
{ "type": "content", "pattern": "(Fluid|prefer|dataProcessing)" }
]
},
{
"name": "condition_syntax_v12",
"prompt": "How do I write a condition in TypoScript that checks if a backend user is logged in (TYPO3 v12+)?",
"assertions": [
{ "type": "content", "pattern": "backend\\.user\\.isLoggedIn" },
{ "type": "content", "pattern": "(Symfony|Expression Language|\\[)" }
]
},
{
"name": "migration_v12_to_v13",
"prompt": "What TypoScript changes are needed when upgrading from TYPO3 v12 to v13?",
"assertions": [
{ "type": "content", "pattern": "(Site Sets|PAGEVIEW|config\\.baseURL)" },
{ "type": "content", "pattern": "(migration|upgrade|replace)" }
]
},
{
"name": "migration_v13_to_v14_conditions",
"prompt": "My TypoScript condition [getTSFE() && getTSFE().id == 42] stopped working after upgrading to TYPO3 v14. Why?",
"assertions": [
{ "type": "content", "pattern": "(removed in v14|getTSFE\\(\\) was removed|107473)" },
{ "type": "content", "pattern": "(page\\[\"uid\"\\]|page\\['uid'\\]|getPageArguments|request)" }
]
},
{
"name": "review_flags_tsfe_getdata_v14",
"prompt": "Review this TYPO3 v14 TypoScript snippet: lib.link = TEXT\nlib.link.typolink.parameter.data = TSFE:id",
"assertions": [
{ "type": "content", "pattern": "(TSFE.*(removed|not available|v14)|v14.*TSFE)" },
{ "type": "content", "pattern": "page\\s*:\\s*uid|page:uid" }
]
}
]
{
"12": {
"content-objects/fluidtemplate": {
"level": "recommended",
"message": "Consider PAGEVIEW (available in v13+) as the modern replacement for FLUIDTEMPLATE.",
"recipe": "page-setup"
},
"frontend/conditions": {
"level": "required",
"message": "Legacy conditions ([globalVar = TS:xx], [browser], [system], [device]) were removed in v12. Use Symfony Expression Language exclusively."
},
"about/usage": {
"level": "recommended",
"message": "Site Sets (introduced in v13) are the modern way to distribute TypoScript. In v12, prefer structured sys_template includes over inline TypoScript blocks."
},
"frontend/config": {
"level": "tip",
"message": "config.no_cache = 1 must never be used in production. Use COA_INT or USER_INT for uncached content objects instead."
},
"content-objects/user": {
"level": "tip",
"message": "USER_INT renders this object on every request (page is cached with placeholders). Use sparingly — each INT object adds per-request overhead."
},
"functions/stdwrap": {
"level": "tip",
"message": "Prefer getData (getText) over stdWrap.field for simple value access — more readable and avoids unnecessary stdWrap overhead."
},
"content-objects/hmenu": {
"level": "recommended",
"message": "Consider MenuProcessor with a Fluid template for new navigation implementations. More flexible than HMENU and aligns with the Fluid-based rendering approach."
},
"content-objects/content": {
"level": "recommended",
"message": "In Fluid-based page setups, prefer DatabaseQueryProcessor over the CONTENT cObject for fetching records into Fluid templates."
},
"backend/page-tsconfig/tceform": {
"level": "tip",
"message": "Use TCEFORM to restrict editors to valid choices instead of relying on validation alone. Reduces editor errors at the source."
}
},
"13": {
"content-objects/fluidtemplate": {
"level": "recommended",
"message": "PAGEVIEW is the recommended page rendering approach in v13+. FLUIDTEMPLATE continues to work but PAGEVIEW requires less configuration.",
"recipe": "page-setup",
"migration": "migration-v12-to-v13.md#1-pageview-as-preferred-page-template-object"
},
"content-objects/pageview": {
"level": "recommended",
"message": "PAGEVIEW is the modern page rendering cObject in v13+. Replaces FLUIDTEMPLATE for page-level templates. Requires a Fluid template with layoutRootPaths and templateRootPaths.",
"recipe": "page-setup"
},
"content-objects/hmenu": {
"level": "recommended",
"message": "Consider MenuProcessor with Fluid for new navigation implementations. More flexible and maintainable than HMENU/TMENU.",
"recipe": "menu-setup"
},
"frontend/config": {
"level": "required",
"message": "config.no_cache = 1 must NEVER be used in production. It disables the page cache entirely. Use COA_INT or USER_INT for uncached content objects.",
"recipe": null
},
"about/usage": {
"level": "recommended",
"message": "Site Sets are the modern way to organize and distribute TypoScript in v13+. They replace sys_template-based static includes. Prefer Site Sets for new extensions and projects.",
"recipe": "site-sets"
},
"frontend/conditions": {
"level": "tip",
"message": "Use Symfony Expression Language for all conditions. Legacy bracket syntax ([globalVar], [browser], etc.) was removed in v12."
},
"functions/stdwrap": {
"level": "tip",
"message": "Prefer getData over stdWrap.field for simple value access. More readable and avoids unnecessary stdWrap overhead."
},
"content-objects/user": {
"level": "tip",
"message": "USER_INT renders this object on every request (page is cached with placeholders). Use sparingly — each INT object adds per-request overhead."
},
"data-processors/database-query-processor": {
"level": "recommended",
"message": "DatabaseQueryProcessor replaces the CONTENT cObject for Fluid-based templates. Cleaner separation of data retrieval and rendering."
},
"content-objects/content": {
"level": "recommended",
"message": "In Fluid-based page setups, prefer DatabaseQueryProcessor over the CONTENT cObject for fetching records into templates."
},
"content-objects/coa": {
"level": "tip",
"message": "COA_INT renders only this object per request while the rest of the page stays cached (placeholder-based). Prefer COA_INT over USER_INT where possible."
},
"functions/typolink": {
"level": "tip",
"message": "For generating links in Fluid templates, prefer the f:uri.typolink and f:link.typolink ViewHelpers over TypoScript typolink to keep logic in Fluid."
},
"backend/page-tsconfig/tceform": {
"level": "tip",
"message": "Use TCEFORM to restrict editors to valid field choices. This prevents invalid data at the source rather than relying on frontend validation."
},
"content-objects/records": {
"level": "tip",
"message": "RECORDS is suited for rendering a fixed set of records by UID. For dynamic queries, use CONTENT or DatabaseQueryProcessor instead."
},
"data-processors/menu-processor": {
"level": "recommended",
"message": "MenuProcessor is the preferred way to build navigations in Fluid-based setups. Replaces HMENU/TMENU for new projects.",
"recipe": "menu-setup"
}
},
"14": {
"content-objects/fluidtemplate": {
"level": "recommended",
"message": "Use PAGEVIEW instead of FLUIDTEMPLATE for page rendering in v14. FLUIDTEMPLATE is legacy for page templates (not officially deprecated) and remains valid for non-page rendering such as emails.",
"recipe": "page-setup",
"migration": "migration-v12-to-v13.md#1-pageview-as-preferred-page-template-object"
},
"content-objects/pageview": {
"level": "recommended",
"message": "PAGEVIEW is the standard page rendering cObject in v14. Use it for all page-level Fluid template rendering.",
"recipe": "page-setup"
},
"about/usage": {
"level": "required",
"message": "Site Sets are the required way to provide TypoScript in v14. sys_template-based static includes from extensions are no longer supported.",
"recipe": "site-sets",
"migration": "migration-v12-to-v13.md#2-site-sets-replacing-sys_template-includes-and-typoscript-constants"
},
"frontend/config": {
"level": "required",
"message": "config.no_cache = 1 must NEVER be used. It disables the page cache entirely. Use COA_INT or USER_INT for uncached content.",
"recipe": null
},
"content-objects/user": {
"level": "tip",
"message": "USER_INT renders this object on every request (page is cached with placeholders). Use sparingly — each INT object adds per-request overhead."
},
"content-objects/hmenu": {
"level": "recommended",
"message": "MenuProcessor with Fluid is the recommended navigation approach in v14. HMENU remains available but is not the preferred pattern for new projects.",
"recipe": "menu-setup"
},
"data-processors/database-query-processor": {
"level": "recommended",
"message": "DatabaseQueryProcessor is the standard way to fetch records for Fluid templates in v14. Prefer over the CONTENT cObject in Fluid-based setups."
},
"frontend/conditions": {
"level": "tip",
"message": "Use Symfony Expression Language for all conditions. getTSFE() was removed in v14 (#107473) — use page, request, site instead. New in v14: the site.locale expression (#107105)."
},
"functions/stdwrap": {
"level": "tip",
"message": "Prefer getData over stdWrap.field for simple value access. More readable and avoids unnecessary stdWrap overhead."
},
"data-processors/menu-processor": {
"level": "recommended",
"message": "MenuProcessor is the preferred way to build navigations in Fluid-based setups in v14.",
"recipe": "menu-setup"
}
}
}
TypoScript Debugging Reference
Common Error Messages
| Error Message | Cause | Solution |
|---|---|---|
| "The page is not configured" | No PAGE object defined or wrong typeNum | Add page = PAGE with page.typeNum = 0 |
| "No TypoScript template found" | Missing sys_template record / Site Set | Create sys_template record with root flag OR configure Site Set |
| "Content Object ... not found" | Typo in cObject name or missing extension | Check spelling (TEXT, IMAGE, COA — case-sensitive) |
| "The TypoScript object path ... is not valid" | Wrong nesting or missing parent object | Check path hierarchy and ensure parent exists |
| "stdWrap ... is not a valid function" | Using stdWrap property on wrong level | Check stdWrap nesting level |
| "Could not find template file" | Wrong templateRootPaths or file name | Verify paths and file naming convention |
| "Fluid template ... not found" | FLUIDTEMPLATE/PAGEVIEW template path wrong | Check templateName and templateRootPaths |
| "No layout found with name ..." | Layout file missing or wrong layoutRootPaths | Create layout file or fix path |
| "Rendering the Content-Object ... returned ..." | Exception in content rendering | Check inner cObject configuration |
| "Page not found (404)" | No page record or wrong domain/language | Check site configuration and page visibility |
| "Access denied (403)" | Frontend user permissions | Check fe_group settings on page/content |
| "TypoScript condition parse error" | Invalid Symfony Expression Language | Check condition syntax, use expression builder |
---
Debugging Tools
Development Mode
# Show full errors instead of custom error pages (DEV ONLY)
config.contentObjectExceptionHandler = 0stdWrap Debugging
lib.myObject = TEXT
lib.myObject {
value = Hello
# Shows stdWrap processing state at this point
debug = 1
# Shows detailed debug output (1 = var_dump, 2 = debug())
debugFunc = 2
# Outputs current data array
debugData = 1
}Admin Panel
# Enable Admin Panel for backend users in frontend
config.admPanel = 1The Admin Panel provides tabs for:
- Preview (page, time, workspace)
- Cache (info and flush)
- TypoScript (object browser, conditions)
- Info (GET/POST data, page info)
TypoScript Object Browser
Located in Backend > Template module > TypoScript Object Browser.
Use to:
- Inspect the compiled TypoScript tree
- Search for specific object paths
- Verify that includes and conditions are applied
System Log
Backend > System > Log module shows TypoScript-related errors and warnings, including template loading failures.
TYPO3 Debug Console
In Development context, exceptions include a stack trace in the browser. Set TYPO3_CONTEXT=Development in your .env or server config.
---
Common Debugging Patterns
"Why is my TypoScript not applied?"
1. Check include order — TypoScript is processed top-to-bottom. A later include overwrites earlier definitions. 2. Check conditions — Use the Admin Panel > TypoScript tab to see which conditions evaluate to true/false. 3. Clear all caches — Backend > Flush all caches (lightning bolt). TypoScript is cached; changes require a cache flush. 4. Check template hierarchy — Sys_template records inherit from parent pages. Verify the root template has the "Root" flag set. 5. Check context — [context("..")] conditions depend on application context (Production, Development).
# Verify a value is set by outputting it directly
page.10 = TEXT
page.10.value = DEBUG: check if this renders"Why is my Fluid template empty?"
1. Check variable passing — Use stdWrap.debug = 1 on the DATA source or add a temporary {f:debug(value: myVar)} in the template. 2. Check DataProcessor output — Add a temporary debug viewhelper to inspect processor results.
<!-- Temporary debug output in Fluid template -->
<f:debug title="All variables">{_all}</f:debug>
<f:debug title="My processor result">{myProcessorVariable}</f:debug>1. Check templateRootPaths — The path must resolve to an existing file. Use absolute EXT: paths or verified filesystem paths. 2. Check templateName — The file name must match exactly (case-sensitive on Linux).
10 = FLUIDTEMPLATE
10 {
templateRootPaths.0 = EXT:my_ext/Resources/Private/Templates/
# templateName resolves to .../Templates/MyTemplate.html
templateName = MyTemplate
}"Why is my page cached incorrectly?"
1. Use COA_INT for dynamic content — COA_INT (and USER_INT) is rendered on every request while the rest of the page stays cached (placeholder-based).
# This content is excluded from page cache
lib.dynamic = COA_INT
lib.dynamic {
10 = TEXT
10.data = date:U
10.strftime = %H:%M:%S
}1. Check no_cache — config.no_cache = 1 disables caching for the entire page. Use sparingly. 2. Check cache tags — Extensions may set cache tags. Flushing tagged caches via the Admin Panel or CLI can resolve stale content. 3. Check condition-dependent content — Conditions evaluated at cache-build time are baked in. Use _INT objects for content that must vary per request.
"Why does my condition not work?"
Conditions use Symfony Expression Language since TYPO3 v10. Legacy TypoScript syntax ([browser = ...]) is no longer supported.
v14: getTSFE() was removed from the condition context (#107473). Conditions like [getTSFE() && getTSFE().id == 42] throw an error or silently never match — rewrite them:
# Wrong (v13 and earlier):
[getTSFE() && getTSFE().id == 42]
# Right (v14):
[page["uid"] == 42]
# or, where the page record is not available:
[request?.getPageArguments()?.getPageId() == 42]Also note: userFunc conditions and callables require explicit opt-in registration in v14 (#108054) — an unregistered callable is skipped without error.
# Correct: Symfony Expression Language
[request.getNormalizedParams().isHttps()]
config.forceAbsoluteUrls = 1
[end]
[traverse(request.getQueryParams(), 'type') == 2]
page.typeNum = 2
[end]
# Page UID condition
[page["uid"] == 42]
# Applied only on page 42
[end]
# Backend user logged in
[backend.user.isLoggedIn]
config.admPanel = 1
[end]
# Site set / application context
[applicationContext == "Development"]
config.contentObjectExceptionHandler = 0
[end]Available condition variables:
page— current page record fieldsrequest— PSR-7 request objectsite— current site configurationsiteLanguage— current site languagefrontend.user— frontend user databackend.user— backend user dataapplicationContext— TYPO3 application context stringtree— page tree info (rootLine, level)
TypoScript & Fluid Best Practices
General patterns and conventions. Not a tutorial — use this as a quick reference.
---
TypoScript Organization
Constants vs Settings
v12: Use constants.typoscript with the Constants Editor in sys_template.
# constants.typoscript
page.logo.file = EXT:my_ext/Resources/Public/Images/logo.svg
page.logo.alt = My Sitev13+: Prefer Site Set settings.definitions.yaml for typed, validated settings. Constants still work but Site Sets are the modern replacement.
# Configuration/Sets/MySet/settings.definitions.yaml
settings:
page.logo.file:
label: Logo file path
type: string
default: EXT:my_ext/Resources/Public/Images/logo.svgFile Structure
v12 (sys_template-based):
Configuration/
TypoScript/
setup.typoscript # Main setup
constants.typoscript # Constants
page/
page.typoscript
content/
content.typoscriptv13+ (Site Sets):
Configuration/
Sets/
MySet/
config.yaml # Set metadata + dependencies
setup.typoscript # Setup TypoScript
constants.typoscript # Constants (backward compat)
settings.definitions.yamlWhen to Split vs Single File
Split into multiple files when:
- A logical section exceeds ~100 lines
- Content types, page config, and navigation each warrant their own file
- Multiple developers work on the same extension
Keep as single file when:
- Simple site package with minimal TypoScript
- Extension plugin configuration only
Include Order and Override Precedence
Later includes override earlier ones. Order within a sys_template or Site Set:
1. Base/framework TypoScript (e.g., fluid_styled_content) 2. Site package setup 3. Page-type overrides 4. Environment-specific overrides (via conditions)
# Explicit override — this wins over earlier definitions
page.10.variables.myVar = overridden value---
Naming Conventions
| Prefix | Purpose | Example |
|---|---|---|
lib.* | Reusable library objects — referenced with =< | lib.breadcrumb, lib.navigation |
temp.* | Temporary objects — discarded after processing | temp.menu = HMENU |
plugin.tx_* | Extbase frontend plugin configuration | plugin.tx_news_pi1.settings.limit = 10 |
module.tx_* | Backend module configuration | module.tx_news.settings.allowedCTypes = text |
tt_content.* | Content element rendering | tt_content.my_ctype =< lib.myRenderer |
Use lib.* for anything referenced from multiple places. temp.* is a convention — TYPO3 does not automatically clean it, but it signals intent.
---
Version-Specific Patterns
v12
- Use
FLUIDTEMPLATEfor page rendering (not yet deprecated) - Distribute TypoScript via
sys_templatestatic includes - Constants via
constants.typoscript+ Constants Editor - Conditions use Symfony Expression Language (legacy bracket syntax removed)
page = PAGE
page.10 = FLUIDTEMPLATE
page.10 {
templateRootPaths.0 = EXT:my_ext/Resources/Private/Templates/
layoutRootPaths.0 = EXT:my_ext/Resources/Private/Layouts/
partialRootPaths.0 = EXT:my_ext/Resources/Private/Partials/
templateName = Default
}v13
PAGEVIEWis the recommended page rendering approach (replacesFLUIDTEMPLATEfor new projects)- Introduce Site Sets (
Configuration/Sets/*/config.yaml) - Use
settings.definitions.yamlfor typed settings PAGEVIEWauto-resolves templates by backend layout name convention
page = PAGE
page.10 = PAGEVIEW
page.10 {
paths.10 = EXT:my_ext/Resources/Private/
}# Configuration/Sets/MySet/config.yaml
name: my-vendor/my-set
label: My Site Set
dependencies:
- typo3/fluid-styled-contentv14
- Site Sets are mandatory for extensions providing TypoScript
PAGEVIEWis the standard for page rendering —FLUIDTEMPLATEis legacy for page templates (not officially deprecated, still valid for non-page rendering such as emails)sys_template-based static includes from extensions no longer supported- All extension TypoScript must be distributed via Site Sets
@importis mandatory —<INCLUDE_TYPOSCRIPT:>was removedgetTSFE()in conditions was removed (#107473) — usepage,request,site; new:site.localeexpression (#107105)- Prefer the
PKG:resource format overEXT:(PKG:my-vendor/package-name:Resources/Public/...);EXT:still works
# v14: PKG: resource format (preferred over EXT:)
page.10.settings.logo = PKG:my-vendor/site-package:Resources/Public/Icons/logo.svg---
When to Use What
TypoScript vs DataProcessors vs PHP Middleware
| Scenario | Use |
|---|---|
| Page structure and rendering configuration | TypoScript |
| Fetching and transforming data for Fluid templates | DataProcessor |
| HTTP request/response manipulation | PHP Middleware |
| Complex business logic | PHP (Controller, Service) |
| Simple value output in templates | Fluid + TypoScript data |
COA vs COA_INT
COA— cached. Use for static or rarely-changing composed output.COA_INT— uncached. Renders on every request, but only this object — the surrounding page remains cached.
# Good: scoped uncached rendering
lib.currentTime = COA_INT
lib.currentTime.10 = TEXT
lib.currentTime.10.data = date:U
lib.currentTime.10.strftime = %H:%MBoth COA_INT and USER_INT render per request while the rest of the page stays cached (placeholder-based). Prefer COA_INT over USER_INT where possible — each INT object adds per-request overhead.
CONTENT cObject vs DatabaseQueryProcessor
CONTENT | DatabaseQueryProcessor | |
|---|---|---|
| Output | Rendered TypoScript | Data array in Fluid variable |
| Use with | TypoScript rendering pipeline | Fluid templates |
| Flexibility | Limited to TypoScript rendering | Full Fluid template control |
Use CONTENT for TypoScript-only rendering pipelines. Use DatabaseQueryProcessor in Fluid-based setups.
# DatabaseQueryProcessor — preferred in Fluid setups
page.10.dataProcessing.10 = TYPO3\CMS\Frontend\DataProcessing\DatabaseQueryProcessor
page.10.dataProcessing.10 {
table = tx_news_domain_model_news
orderBy = datetime DESC
max = 5
as = latestNews
}stdWrap vs Fluid ViewHelpers
- Use
stdWrapwhen transforming values within a TypoScript rendering pipeline - Use Fluid ViewHelpers when the value is already in a template variable
# stdWrap — appropriate in TypoScript context
lib.pageTitle = TEXT
lib.pageTitle.data = page:title
lib.pageTitle.wrap = <h1>|</h1>
lib.pageTitle.htmlSpecialChars = 1<!-- Fluid — appropriate in templates -->
<h1>{data.title -> f:format.htmlspecialchars()}</h1>---
Fluid Best Practices
No Logic in Templates
Move conditions and data transformation to DataProcessors or ViewHelpers. Templates should only render, not compute.
<!-- Bad: logic in template -->
<f:if condition="{item.items -> f:count()} > 3">...</f:if>
<!-- Good: pre-computed in DataProcessor -->
<f:if condition="{hasEnoughItems}">...</f:if>Labels
Always use f:translate for user-facing strings. Never hardcode strings.
<f:translate key="LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:my.label" />
<!-- With arguments -->
<f:translate key="LLL:EXT:my_ext/Resources/Private/Language/locallang.xlf:items.count"
arguments="{0: items -> f:count()}" />Layouts and Partials
- Layouts: Define the outer HTML structure (header, footer, main). One layout per page type.
- Partials: Reusable fragments (cards, navigation items, form fields).
- Templates: The entry point — extends a layout, calls partials.
<!-- Template -->
<f:layout name="Default" />
<f:section name="Main">
<f:render partial="Card" arguments="{item: item}" />
</f:section>Avoid duplicating markup — extract partials aggressively.
Escaping
Fluid escapes output by default. Only bypass escaping when you have verified the content is safe (e.g., sanitized HTML from RTE).
<!-- Auto-escaped — safe by default -->
{item.title}
<!-- Raw output — only for trusted, pre-sanitized HTML -->
<f:format.raw>{item.bodytext}</f:format.raw>Never use f:format.raw for user-generated content without prior sanitization.
Components (v13+)
Fluid Components (introduced in TYPO3 v13 via typo3fluid/fluid 4.x) provide self-contained, reusable UI elements with explicit parameter declarations.
<!-- Component definition: Resources/Private/Components/Card.html -->
<fc:component>
<fc:param name="title" type="string" />
<fc:param name="image" type="TYPO3\CMS\Extbase\Domain\Model\FileReference" optional="true" />
<fc:renderer>
<article class="card">
<h2>{title}</h2>
</article>
</fc:renderer>
</fc:component>
<!-- Usage -->
<fc:render component="EXT:my_ext/Resources/Private/Components/Card"
title="{item.title}" />Use Components (v13+) for new self-contained UI elements. Use traditional Partials for simpler fragments or when targeting v12 compatibility.
---
Copy vs Reference
| Operator | Behaviour |
|---|---|
< | Copy — independent copy at the time of assignment. Changes to the original after this point do not affect the copy. |
=< | Reference — linked to the original object. Always reflects the current state of the referenced object. |
> | Unset — removes the property or object entirely. |
# Copy — independent from lib.myObject after this line
page.10 < lib.myObject
# Reference — always uses the current state of lib.navigation
page.5 =< lib.navigation
# Unset — removes the property
page.10.wrap >Use =< (reference) for lib.* objects to ensure a single source of truth. Use < (copy) when you need an independent variant that you will modify separately.
Recipe: AJAX/JSON Endpoint via TypoScript PAGE typeNum
Version: v12+
What this builds
A JSON API endpoint using a separate PAGE type with a custom typeNum, suitable for AJAX requests from frontend JavaScript, returning structured JSON data with proper headers.
TypoScript -- JSON Endpoint
# JSON API endpoint (typeNum = 1638)
ajaxPage = PAGE
ajaxPage {
typeNum = 1638
config {
disableAllHeaderCode = 1
additionalHeaders {
10.header = Content-Type: application/json; charset=utf-8
20.header = Cache-Control: no-cache, no-store, must-revalidate
30.header = Access-Control-Allow-Origin: https://www.example.com
40.header = Access-Control-Allow-Methods: GET, POST
50.header = Access-Control-Allow-Headers: Content-Type, X-Requested-With
}
debug = 0
no_cache = 1
admPanel = 0
}
# Simple: Return content elements as JSON
10 = CONTENT
10 {
table = tt_content
select {
pidInList = this
where = {#colPos} = 0
orderBy = sorting
}
renderObj = COA
renderObj {
10 = TEXT
10.field = uid
10.wrap = "uid":|,
20 = TEXT
20.field = header
20.htmlSpecialChars = 1
20.wrap = "header":"|",
30 = TEXT
30.field = CType
30.htmlSpecialChars = 1
30.wrap = "type":"|",
40 = TEXT
40.field = bodytext
40.htmlSpecialChars = 1
40.replacement {
10 {
search = "
replace = \"
}
20 {
search.char = 10
replace = \n
}
30 {
search.char = 13
replace =
}
}
40.wrap = "bodytext":"|"
stdWrap.noTrimWrap = |{|},|
}
stdWrap.wrap = {"status":"ok","items":[|]}
stdWrap.trimRight = ,
# Wrap items array properly (remove trailing comma)
stdWrap.replacement {
10 {
search = ,]}
replace = ]}
}
}
}
}TypoScript -- Search Endpoint
# Search API endpoint (typeNum = 1639)
searchApi = PAGE
searchApi {
typeNum = 1639
config {
disableAllHeaderCode = 1
additionalHeaders {
10.header = Content-Type: application/json; charset=utf-8
20.header = Cache-Control: private, max-age=300
}
debug = 0
admPanel = 0
}
10 = COA
10 {
# Check if search query parameter exists
10 = LOAD_REGISTER
10 {
searchQuery.data = GP:q
searchQuery.htmlSpecialChars = 1
}
# Return results
20 = CONTENT
20 {
table = pages
select {
pidInList = 1
recursive = 99
where.dataWrap = ({#title} LIKE '%{register:searchQuery}%' OR {#description} LIKE '%{register:searchQuery}%')
andWhere = {#hidden} = 0 AND {#deleted} = 0 AND {#doktype} IN (1,2)
max = 10
orderBy = title
}
renderObj = COA
renderObj {
10 = TEXT
10.field = uid
10.wrap = "uid":|,
20 = TEXT
20.field = title
20.htmlSpecialChars = 1
20.wrap = "title":"|",
30 = TEXT
30 {
field = description
htmlSpecialChars = 1
crop = 150|...|1
wrap = "description":"|",
}
40 = TEXT
40 {
typolink {
parameter.field = uid
returnLast = url
forceAbsoluteUrl = 1
}
wrap = "url":"|"
}
stdWrap.noTrimWrap = |{|},|
}
stdWrap {
wrap = {"status":"ok","query":"{register:searchQuery}","results":[|]}
insertData = 1
replacement {
10 {
search = ,]}
replace = ]}
}
}
}
# No results fallback
ifEmpty.cObject = TEXT
ifEmpty.cObject {
value = {"status":"ok","query":"{register:searchQuery}","results":[]}
insertData = 1
}
}
}
}TypoScript -- Navigation Data Endpoint
# Navigation API (typeNum = 1640)
navApi = PAGE
navApi {
typeNum = 1640
config {
disableAllHeaderCode = 1
additionalHeaders {
10.header = Content-Type: application/json; charset=utf-8
20.header = Cache-Control: public, max-age=3600
}
debug = 0
admPanel = 0
}
10 = USER
10 {
userFunc = TYPO3\CMS\Extbase\Core\Bootstrap->run
extensionName = SitePackage
pluginName = NavigationApi
vendorName = Vendor
}
}JavaScript -- Frontend AJAX Call
class AjaxContent {
constructor(containerId, pageUid) {
this.container = document.getElementById(containerId);
this.pageUid = pageUid;
this.apiUrl = window.location.origin;
}
async loadContent() {
try {
const url = new URL(this.apiUrl);
url.searchParams.set('id', this.pageUid);
url.searchParams.set('type', '1638');
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.status === 'ok') {
this.renderItems(data.items);
}
} catch (error) {
console.error('Failed to load content:', error);
this.container.textContent = 'Content could not be loaded.';
}
}
renderItems(items) {
this.container.replaceChildren();
items.forEach(item => {
const article = document.createElement('article');
article.className = 'ajax-item';
article.dataset.uid = item.uid;
const heading = document.createElement('h3');
heading.textContent = item.header;
article.appendChild(heading);
const content = document.createElement('div');
content.className = 'ajax-item__content';
content.textContent = item.bodytext;
article.appendChild(content);
this.container.appendChild(article);
});
}
}
// Search autocomplete
class SearchAutocomplete {
constructor(inputId, resultsId) {
this.input = document.getElementById(inputId);
this.results = document.getElementById(resultsId);
this.debounceTimer = null;
this.input.addEventListener('input', () => this.onInput());
}
onInput() {
clearTimeout(this.debounceTimer);
const query = this.input.value.trim();
if (query.length < 3) {
this.results.replaceChildren();
return;
}
this.debounceTimer = setTimeout(() => this.search(query), 300);
}
async search(query) {
const url = new URL(window.location.origin);
url.searchParams.set('type', '1639');
url.searchParams.set('q', query);
const response = await fetch(url.toString());
const data = await response.json();
this.renderResults(data.results);
}
renderResults(results) {
this.results.replaceChildren();
if (results.length === 0) {
const noResults = document.createElement('li');
noResults.className = 'no-results';
noResults.textContent = 'No results found';
this.results.appendChild(noResults);
return;
}
results.forEach(result => {
const li = document.createElement('li');
li.className = 'search-result';
const link = document.createElement('a');
link.href = result.url;
const title = document.createElement('strong');
title.textContent = result.title;
link.appendChild(title);
const desc = document.createElement('span');
desc.textContent = result.description;
link.appendChild(desc);
li.appendChild(link);
this.results.appendChild(li);
});
}
}Site Configuration -- Route Enhancer (optional)
File: config/sites/main/config.yaml (append to existing)
routeEnhancers:
JsonApi:
type: PageType
map:
api.json: 1638
search.json: 1639
nav.json: 1640This maps /api.json to ?type=1638, making URLs cleaner.
Notes
disableAllHeaderCode = 1removes the entire HTML structure (doctype, html, head, body tags) -- essential for non-HTML responses.- Always set
Content-Type: application/jsonexplicitly. Without it, browsers may misinterpret the response. no_cache = 1disables TYPO3's page cache for the endpoint. Use sparingly -- for search endpoints it makes sense, for static data useCache-Controlheaders.- CORS headers (
Access-Control-Allow-Origin) are only needed for cross-origin requests. For same-origin AJAX, they are unnecessary. - Use specific origin values instead of
*forAccess-Control-Allow-Originin production. - The
PageTyperoute enhancer creates clean URLs for typeNum pages. The file extension (.json) helps with content negotiation. - For complex JSON responses, consider using an Extbase controller with
JsonViewinstead of building JSON via TypoScript string concatenation. GP:qreads GET/POST parameterq. Always sanitize viahtmlSpecialCharsbefore using in queries.- The
where.dataWrapapproach uses query builder syntax internally, but for complex queries an Extbase/Doctrine approach is safer. - typeNum values should be unique across the installation. Common convention: use 4-digit numbers above 1000.
Recipe: Breadcrumb Navigation
Version: v12+
What this builds
A breadcrumb navigation using the MenuProcessor with rootline type, including a Fluid template with accessible markup and JSON-LD structured data for SEO.
TypoScript
page.10 {
dataProcessing {
40 = TYPO3\CMS\Frontend\DataProcessing\MenuProcessor
40 {
special = rootline
special.range = 0|-1
includeNotInMenu = 0
as = breadcrumb
# Remove spacer pages from breadcrumb
excludeDoktypes = 199,254
}
}
}Fluid Template
File: EXT:site_package/Resources/Private/Partials/Navigation/Breadcrumb.html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:if condition="{breadcrumb -> f:count()} > 1">
<nav class="breadcrumb" aria-label="Breadcrumb">
<ol class="breadcrumb__list" itemscope itemtype="https://schema.org/BreadcrumbList">
<f:for each="{breadcrumb}" as="item" iteration="iterator">
<li class="breadcrumb__item{f:if(condition: item.current, then: ' breadcrumb__item--current')}"
itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<f:if condition="{item.current}">
<f:then>
<span class="breadcrumb__text" itemprop="name" aria-current="page">
{item.title}
</span>
</f:then>
<f:else>
<a href="{item.link}" class="breadcrumb__link" itemprop="item">
<span itemprop="name">{item.title}</span>
</a>
</f:else>
</f:if>
<meta itemprop="position" content="{iterator.cycle}" />
<f:if condition="!{item.current}">
<span class="breadcrumb__separator" aria-hidden="true">/</span>
</f:if>
</li>
</f:for>
</ol>
</nav>
<f:comment><!-- JSON-LD structured data for breadcrumb --></f:comment>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [<f:for each="{breadcrumb}" as="item" iteration="iterator">
{
"@type": "ListItem",
"position": {iterator.cycle},
"name": "{item.title -> f:format.htmlentitiesDecode()}",
"item": "<f:if condition="{item.current}"><f:then>{f:uri.page(pageUid: item.data.uid, absolute: 1)}</f:then><f:else>{item.link}</f:else></f:if>"
}<f:if condition="!{iterator.isLast}">,</f:if></f:for>
]
}
</script>
</f:if>
</html>Alternative: Classic HMENU Breadcrumb
lib.breadcrumb = HMENU
lib.breadcrumb {
special = rootline
special.range = 0|-1
1 = TMENU
1 {
NO {
wrapItemAndSub = <li class="breadcrumb__item">|</li>
ATagTitle.field = nav_title // title
stdWrap.htmlSpecialChars = 1
}
CUR = 1
CUR {
wrapItemAndSub = <li class="breadcrumb__item breadcrumb__item--current" aria-current="page">|</li>
doNotLinkIt = 1
stdWrap.htmlSpecialChars = 1
}
wrap = <nav aria-label="Breadcrumb"><ol class="breadcrumb__list">|</ol></nav>
}
}Breadcrumb with Home Icon
File: EXT:site_package/Resources/Private/Partials/Navigation/BreadcrumbWithHome.html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:if condition="{breadcrumb -> f:count()} > 1">
<nav class="breadcrumb" aria-label="Breadcrumb">
<ol class="breadcrumb__list">
<f:for each="{breadcrumb}" as="item" iteration="iterator">
<li class="breadcrumb__item{f:if(condition: item.current, then: ' breadcrumb__item--current')}">
<f:if condition="{item.current}">
<f:then>
<span class="breadcrumb__text" aria-current="page">
{item.title}
</span>
</f:then>
<f:else>
<a href="{item.link}" class="breadcrumb__link">
<f:if condition="{iterator.isFirst}">
<f:then>
<span class="breadcrumb__home-icon" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
</span>
<span class="sr-only">Home</span>
</f:then>
<f:else>
{item.title}
</f:else>
</f:if>
</a>
<span class="breadcrumb__separator" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</span>
</f:else>
</f:if>
</li>
</f:for>
</ol>
</nav>
</f:if>
</html>Notes
special.range = 0|-1starts from the root page (0) and goes to the current page (-1). Use1|-1to skip the root page.- The
breadcrumbarray fromMenuProcessorprovidestitle,link,active,current, anddata(full page record) for each item. - Only show the breadcrumb when there are more than one item (root + current page at minimum) to avoid showing just "Home".
- JSON-LD structured data for breadcrumbs is recommended by Google. It appears as breadcrumb rich snippets in search results.
- Microdata attributes (
itemscope,itemprop) in the HTML provide an additional structured data signal but JSON-LD is preferred. aria-label="Breadcrumb"andaria-current="page"on the current item ensure screen reader accessibility.nav_title // titlein HMENU uses the navigation title if set, falling back to the page title.- For the JSON-LD output, use
f:format.htmlentitiesDecode()to ensure clean text without HTML entities in the JSON.
Recipe: Override fluid_styled_content Templates
Version: v12+
What this builds
Custom Fluid template overrides for fluid_styled_content, replacing the default rendering of standard content elements like textmedia, header, and others.
TypoScript — Override Template Paths
# Override all fluid_styled_content templates
lib.contentElement {
templateRootPaths {
# Default (from fluid_styled_content)
# 0 = EXT:fluid_styled_content/Resources/Private/Templates/
# Override with custom templates
20 = EXT:site_package/Resources/Private/Templates/ContentElements/
}
partialRootPaths {
# 0 = EXT:fluid_styled_content/Resources/Private/Partials/
20 = EXT:site_package/Resources/Private/Partials/ContentElements/
}
layoutRootPaths {
# 0 = EXT:fluid_styled_content/Resources/Private/Layouts/
20 = EXT:site_package/Resources/Private/Layouts/ContentElements/
}
}Override: Textmedia Content Element
File: EXT:site_package/Resources/Private/Templates/ContentElements/Textmedia.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">
<div class="ce-textmedia ce-textmedia--position-{data.imageorient}
{f:if(condition: '{data.imagecols} > 1', then: 'ce-textmedia--gallery')}">
<f:if condition="{data.header}">
<f:render partial="Header/All" arguments="{_all}" />
</f:if>
<f:if condition="{files}">
<div class="ce-textmedia__media ce-textmedia__media--cols-{data.imagecols}">
<f:for each="{files}" as="file" iteration="iterator">
<div class="ce-textmedia__media-item">
<f:if condition="{file.type} == 2">
<f:then>
<figure class="ce-textmedia__figure">
<f:image image="{file}"
width="{f:if(condition: '{data.imagewidth}', then: '{data.imagewidth}', else: '1200')}"
alt="{file.alternative}"
title="{file.title}"
loading="{f:if(condition: '{iterator.isFirst}', then: 'eager', else: 'lazy')}"
class="ce-textmedia__image" />
<f:if condition="{file.description}">
<figcaption class="ce-textmedia__caption">
{file.description}
</figcaption>
</f:if>
</figure>
</f:then>
<f:else if="{file.type} == 4">
<f:media file="{file}"
width="800"
alt="{file.alternative}"
class="ce-textmedia__video" />
</f:else>
</f:if>
</div>
</f:for>
</div>
</f:if>
<f:if condition="{data.bodytext}">
<div class="ce-textmedia__text">
<f:format.html>{data.bodytext}</f:format.html>
</div>
</f:if>
</div>
</f:section>
</html>Override: Header Partial
File: EXT:site_package/Resources/Private/Partials/ContentElements/Header/All.html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:if condition="{data.header}">
<div class="ce-header ce-header--layout-{data.header_layout}
{f:if(condition: '{data.header_position}', then: 'ce-header--align-{data.header_position}')}">
<f:if condition="{data.header_layout} != 100">
<f:variable name="headerTag">{f:if(condition: '{data.header_layout}', then: 'h{data.header_layout}', else: 'h2')}</f:variable>
<{headerTag} class="ce-header__title">
<f:if condition="{data.header_link}">
<f:then>
<f:link.typolink parameter="{data.header_link}">{data.header}</f:link.typolink>
</f:then>
<f:else>
{data.header}
</f:else>
</f:if>
</{headerTag}>
</f:if>
<f:if condition="{data.subheader}">
<p class="ce-header__subtitle">{data.subheader}</p>
</f:if>
<f:if condition="{data.date}">
<time class="ce-header__date"
datetime="{f:format.date(date: data.date, format: 'Y-m-d')}">
<f:format.date date="{data.date}" format="%e. %B %Y" />
</time>
</f:if>
</div>
</f:if>
</html>Override: Default Layout
File: EXT:site_package/Resources/Private/Layouts/ContentElements/Default.html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:if condition="{data.frame_class} != 'none'">
<div id="c{data.uid}"
class="ce-frame ce-frame--type-{data.CType}
ce-frame--layout-{data.layout}
{f:if(condition: '{data.frame_class}', then: 'ce-frame--{data.frame_class}')}
{f:if(condition: '{data.space_before_class}', then: 'ce-frame--space-before-{data.space_before_class}')}
{f:if(condition: '{data.space_after_class}', then: 'ce-frame--space-after-{data.space_after_class}')}"
{f:if(condition: data._LOCALIZED_UID, then: 'data-ce-uid="{data._LOCALIZED_UID}"')}>
<f:render section="Main" />
</div>
</f:if>
<f:if condition="{data.frame_class} == 'none'">
<f:render section="Main" />
</f:if>
</html>TypoScript — Add Custom Data Processing
# Add a custom DataProcessor to textmedia
tt_content.textmedia {
dataProcessing {
# FilesProcessor is already at key 10 by default
# Add a GalleryProcessor for image grid calculations
20 = TYPO3\CMS\Frontend\DataProcessing\GalleryProcessor
20 {
maxGalleryWidth = 1200
maxGalleryWidthInText = 600
columnSpacing = 10
borderWidth = 0
borderPadding = 0
as = gallery
}
}
}
# Override rendering for a specific CType completely
tt_content.text {
templateName = Text
dataProcessing {
10 = TYPO3\CMS\Frontend\DataProcessing\FlexFormProcessor
10 {
fieldName = pi_flexform
as = flexformData
}
}
}Override: Bullet List Content Element
File: EXT:site_package/Resources/Private/Templates/ContentElements/Bullets.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">
<f:if condition="{data.header}">
<f:render partial="Header/All" arguments="{_all}" />
</f:if>
<f:switch expression="{data.bullets_type}">
<f:case value="0">
<ul class="ce-bullets ce-bullets--unordered">
<f:for each="{bullets}" as="bullet">
<li class="ce-bullets__item">{bullet}</li>
</f:for>
</ul>
</f:case>
<f:case value="1">
<ol class="ce-bullets ce-bullets--ordered">
<f:for each="{bullets}" as="bullet">
<li class="ce-bullets__item">{bullet}</li>
</f:for>
</ol>
</f:case>
<f:case value="2">
<dl class="ce-bullets ce-bullets--definition">
<f:for each="{bullets}" as="bullet">
<dt class="ce-bullets__term">{bullet.0}</dt>
<dd class="ce-bullets__definition">{bullet.1}</dd>
</f:for>
</dl>
</f:case>
</f:switch>
</f:section>
</html>Notes
- Template paths use numeric keys for priority. Higher numbers override lower numbers. The original fluid_styled_content uses key
0, so use20or higher for overrides. - Only override the templates you need to change. Unoverridden templates fall back to the original fluid_styled_content version.
- Template names must match the CType:
Textmedia.htmlfortextmedia,Text.htmlfortext,Bullets.htmlforbullets, etc. header_layout = 100is the convention for "hidden header" in TYPO3. Check for it to suppress header rendering.f:format.htmlapplies thelib.parseFunc_RTEprocessing to bodytext, which handles links, special chars, and other transformations.- Always use
f:imageorf:mediaViewHelpers instead of raw<img>tags to get proper FAL processing and crop support. - The
datavariable contains all fields from thett_contentrecord of the current content element. - In v13+ with PAGEVIEW,
lib.contentElementtemplate path configuration remains the same — only the page template resolution changes.
Recipe: Custom Content Element from Scratch
Version: v12+
What this builds
A complete custom content element "Team Member Card" with TCA registration, database fields, TypoScript rendering with DataProcessors, and Fluid template.
Database Schema
File: EXT:site_package/ext_tables.sql
CREATE TABLE tt_content (
tx_sitepackage_person_name varchar(255) DEFAULT '' NOT NULL,
tx_sitepackage_person_position varchar(255) DEFAULT '' NOT NULL,
tx_sitepackage_person_email varchar(255) DEFAULT '' NOT NULL,
tx_sitepackage_person_phone varchar(100) DEFAULT '' NOT NULL,
tx_sitepackage_person_linkedin varchar(255) DEFAULT '' NOT NULL,
);TCA — Register Content Element Type
File: EXT:site_package/Configuration/TCA/Overrides/tt_content.php
<?php
declare(strict_types=1);
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
defined('TYPO3') or die();
// Register the CType
ExtensionManagementUtility::addTcaSelectItem(
'tt_content',
'CType',
[
'label' => 'LLL:EXT:site_package/Resources/Private/Language/locallang_be.xlf:ctype.team_member',
'value' => 'sitepackage_team_member',
'icon' => 'content-person',
'group' => 'special',
]
);
// Configure fields for this CType
$GLOBALS['TCA']['tt_content']['types']['sitepackage_team_member'] = [
'showitem' => '
--div--;LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.type,
CType,
--div--;LLL:EXT:site_package/Resources/Private/Language/locallang_be.xlf:tab.person,
tx_sitepackage_person_name,
tx_sitepackage_person_position,
tx_sitepackage_person_email,
tx_sitepackage_person_phone,
tx_sitepackage_person_linkedin,
--linebreak--,
image,
bodytext;LLL:EXT:site_package/Resources/Private/Language/locallang_be.xlf:field.biography,
--div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.appearance,
--palette--;;frames,
--palette--;;appearanceLinks,
--div--;LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.access,
--palette--;;hidden,
--palette--;;access,
',
'columnsOverrides' => [
'bodytext' => [
'config' => [
'enableRichtext' => true,
'richtextConfiguration' => 'minimal',
],
],
'image' => [
'config' => [
'maxitems' => 1,
'overrideChildTca' => [
'columns' => [
'crop' => [
'config' => [
'cropVariants' => [
'default' => [
'title' => 'Portrait',
'allowedAspectRatios' => [
'1:1' => [
'title' => '1:1',
'value' => 1.0,
],
],
'selectedRatio' => '1:1',
],
],
],
],
],
],
],
],
],
];
// Configure custom columns
$additionalColumns = [
'tx_sitepackage_person_name' => [
'label' => 'LLL:EXT:site_package/Resources/Private/Language/locallang_be.xlf:field.person_name',
'config' => [
'type' => 'input',
'size' => 50,
'max' => 255,
'required' => true,
],
],
'tx_sitepackage_person_position' => [
'label' => 'LLL:EXT:site_package/Resources/Private/Language/locallang_be.xlf:field.person_position',
'config' => [
'type' => 'input',
'size' => 50,
'max' => 255,
],
],
'tx_sitepackage_person_email' => [
'label' => 'LLL:EXT:site_package/Resources/Private/Language/locallang_be.xlf:field.person_email',
'config' => [
'type' => 'email',
],
],
'tx_sitepackage_person_phone' => [
'label' => 'LLL:EXT:site_package/Resources/Private/Language/locallang_be.xlf:field.person_phone',
'config' => [
'type' => 'input',
'size' => 30,
'max' => 100,
],
],
'tx_sitepackage_person_linkedin' => [
'label' => 'LLL:EXT:site_package/Resources/Private/Language/locallang_be.xlf:field.person_linkedin',
'config' => [
'type' => 'link',
'allowedTypes' => ['url'],
],
],
];
ExtensionManagementUtility::addTCAcolumns('tt_content', $additionalColumns);Backend Preview (PageTSconfig)
File: EXT:site_package/Configuration/page.tsconfig (or via Site Set)
mod.wizards.newContentElement.wizardItems.special {
elements {
sitepackage_team_member {
iconIdentifier = content-person
title = LLL:EXT:site_package/Resources/Private/Language/locallang_be.xlf:ctype.team_member
description = LLL:EXT:site_package/Resources/Private/Language/locallang_be.xlf:ctype.team_member.description
tt_content_defValues {
CType = sitepackage_team_member
}
}
}
show := addToList(sitepackage_team_member)
}TypoScript — Rendering Configuration
tt_content.sitepackage_team_member =< lib.contentElement
tt_content.sitepackage_team_member {
templateName = TeamMember
dataProcessing {
10 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
10 {
references.fieldName = image
as = images
}
}
}Fluid Template
File: EXT:site_package/Resources/Private/Templates/ContentElements/TeamMember.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">
<div class="ce-team-member" itemscope itemtype="https://schema.org/Person">
<f:if condition="{images}">
<div class="ce-team-member__image">
<f:for each="{images}" as="image" iteration="iterator">
<f:if condition="{iterator.isFirst}">
<f:image image="{image}"
width="300c"
height="300c"
alt="{data.tx_sitepackage_person_name}"
class="ce-team-member__photo"
loading="lazy"
itemprop="image" />
</f:if>
</f:for>
</div>
</f:if>
<div class="ce-team-member__info">
<h3 class="ce-team-member__name" itemprop="name">
{data.tx_sitepackage_person_name}
</h3>
<f:if condition="{data.tx_sitepackage_person_position}">
<p class="ce-team-member__position" itemprop="jobTitle">
{data.tx_sitepackage_person_position}
</p>
</f:if>
<ul class="ce-team-member__contact">
<f:if condition="{data.tx_sitepackage_person_email}">
<li class="ce-team-member__contact-item">
<a href="mailto:{data.tx_sitepackage_person_email}"
class="ce-team-member__email"
itemprop="email">
{data.tx_sitepackage_person_email}
</a>
</li>
</f:if>
<f:if condition="{data.tx_sitepackage_person_phone}">
<li class="ce-team-member__contact-item">
<a href="tel:{data.tx_sitepackage_person_phone}"
class="ce-team-member__phone"
itemprop="telephone">
{data.tx_sitepackage_person_phone}
</a>
</li>
</f:if>
<f:if condition="{data.tx_sitepackage_person_linkedin}">
<li class="ce-team-member__contact-item">
<f:link.typolink parameter="{data.tx_sitepackage_person_linkedin}"
class="ce-team-member__linkedin"
additionalAttributes="{itemprop: 'sameAs'}">
LinkedIn Profile
</f:link.typolink>
</li>
</f:if>
</ul>
<f:if condition="{data.bodytext}">
<div class="ce-team-member__biography" itemprop="description">
<f:format.html>{data.bodytext}</f:format.html>
</div>
</f:if>
</div>
</div>
</f:section>
</html>Language Labels
File: EXT:site_package/Resources/Private/Language/locallang_be.xlf
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="messages" date="2024-01-01T00:00:00Z" product-name="site_package">
<body>
<trans-unit id="ctype.team_member">
<source>Team Member Card</source>
</trans-unit>
<trans-unit id="ctype.team_member.description">
<source>Displays a team member card with photo, contact details, and biography.</source>
</trans-unit>
<trans-unit id="tab.person">
<source>Person Details</source>
</trans-unit>
<trans-unit id="field.person_name">
<source>Full Name</source>
</trans-unit>
<trans-unit id="field.person_position">
<source>Position / Job Title</source>
</trans-unit>
<trans-unit id="field.person_email">
<source>Email Address</source>
</trans-unit>
<trans-unit id="field.person_phone">
<source>Phone Number</source>
</trans-unit>
<trans-unit id="field.person_linkedin">
<source>LinkedIn Profile URL</source>
</trans-unit>
<trans-unit id="field.biography">
<source>Biography</source>
</trans-unit>
</body>
</file>
</xliff>File: EXT:site_package/Resources/Private/Language/de.locallang_be.xlf
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="de" datatype="plaintext" original="messages" date="2024-01-01T00:00:00Z" product-name="site_package">
<body>
<trans-unit id="ctype.team_member">
<source>Team Member Card</source>
<target>Teammitglied-Karte</target>
</trans-unit>
<trans-unit id="ctype.team_member.description">
<source>Displays a team member card with photo, contact details, and biography.</source>
<target>Zeigt eine Teammitglied-Karte mit Foto, Kontaktdaten und Biografie.</target>
</trans-unit>
<trans-unit id="tab.person">
<source>Person Details</source>
<target>Personendetails</target>
</trans-unit>
<trans-unit id="field.person_name">
<source>Full Name</source>
<target>Vollständiger Name</target>
</trans-unit>
<trans-unit id="field.person_position">
<source>Position / Job Title</source>
<target>Position / Berufsbezeichnung</target>
</trans-unit>
<trans-unit id="field.person_email">
<source>Email Address</source>
<target>E-Mail-Adresse</target>
</trans-unit>
<trans-unit id="field.person_phone">
<source>Phone Number</source>
<target>Telefonnummer</target>
</trans-unit>
<trans-unit id="field.person_linkedin">
<source>LinkedIn Profile URL</source>
<target>LinkedIn-Profil-URL</target>
</trans-unit>
<trans-unit id="field.biography">
<source>Biography</source>
<target>Biografie</target>
</trans-unit>
</body>
</file>
</xliff>Icon Registration (optional)
File: EXT:site_package/Configuration/Icons.php (v12+)
<?php
declare(strict_types=1);
use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider;
return [
'content-person' => [
'provider' => SvgIconProvider::class,
'source' => 'EXT:site_package/Resources/Public/Icons/ContentElements/team-member.svg',
],
];Notes
- The
=< lib.contentElementoperator copies the base content element configuration (from fluid_styled_content) which includes the Default layout and standard frame wrapping. - Custom fields must be prefixed with
tx_extensionkey_to avoid conflicts with other extensions. - The
type=>emailTCA type (v12+) provides built-in email validation. For v11, usetype=>inputwitheval=>email. - The
type=>linkTCA type (v12+) replaces the oldrenderType=>inputLink. TheallowedTypesoption restricts which link types are available. - Always use
locallang.xlflabels for all backend-visible strings. Never hardcode labels in TCA. - The
FilesProcessormakes FAL file references available as proper File objects in Fluid, enabling crop variants and image processing. - Run
vendor/bin/typo3 extension:setupor clear caches and update the database schema after addingext_tables.sqlfields. - In v13+, register the content element in the new content element wizard via Site Sets
page.tsconfiginstead of manual TSconfig.
Recipe: 404/403 Error Page Setup
Version: v12+
What this builds
Custom error page handling for 404 (Not Found), 403 (Forbidden), and 500 (Server Error) responses using TYPO3's site configuration and TypoScript-based error page rendering.
Site Configuration
File: config/sites/main/config.yaml
rootPageId: 1
base: 'https://www.example.com/'
errorHandling:
-
errorCode: 404
errorHandler: Page
errorContentSource: 't3://page?uid=50'
-
errorCode: 403
errorHandler: Page
errorContentSource: 't3://page?uid=51'
-
errorCode: 500
errorHandler: Page
errorContentSource: 't3://page?uid=52'
-
errorCode: 0
errorHandler: Page
errorContentSource: 't3://page?uid=52'
languages:
-
title: English
languageId: 0
base: /
locale: en_US.UTF-8
errorHandling:
-
errorCode: 404
errorHandler: Page
errorContentSource: 't3://page?uid=50'
-
errorCode: 403
errorHandler: Page
errorContentSource: 't3://page?uid=51'
-
title: Deutsch
languageId: 1
base: /de/
locale: de_DE.UTF-8
errorHandling:
-
errorCode: 404
errorHandler: Page
errorContentSource: 't3://page?uid=60'
-
errorCode: 403
errorHandler: Page
errorContentSource: 't3://page?uid=61'Error Handler Types
Page Handler (recommended)
Renders a TYPO3 page as error content:
errorHandling:
-
errorCode: 404
errorHandler: Page
errorContentSource: 't3://page?uid=50'PHP Handler (custom class)
Uses a custom PHP class for error handling:
errorHandling:
-
errorCode: 404
errorHandler: PHP
errorPhpClassFQCN: Vendor\SitePackage\Error\NotFoundHandlerFluid Handler
Renders a Fluid template directly:
errorHandling:
-
errorCode: 404
errorHandler: Fluid
errorFluidTemplate: 'EXT:site_package/Resources/Private/Templates/Error/404.html'
errorFluidTemplatesRootPath: 'EXT:site_package/Resources/Private/Templates/Error/'
errorFluidLayoutsRootPath: 'EXT:site_package/Resources/Private/Layouts/Error/'
errorFluidPartialsRootPath: 'EXT:site_package/Resources/Private/Partials/Error/'TypoScript — Error Page Template
The error page (uid=50) uses the normal page rendering. Add specific content for the 404 page:
# Error page-specific configuration
[traverse(page, "uid") == 50]
page.meta.robots = noindex, nofollow
# Add search form to help users find what they need
lib.errorSearch = COA
lib.errorSearch {
10 = TEXT
10.value = <div class="error-search">
20 = COA
20 {
10 = TEXT
10.value = <form action="/search" method="get" class="error-search__form">
20 = TEXT
20.value = <label for="error-search-input" class="error-search__label">Search our site:</label>
30 = TEXT
30.value = <input type="search" id="error-search-input" name="tx_solr[q]" class="error-search__input" placeholder="What are you looking for?">
40 = TEXT
40.value = <button type="submit" class="error-search__button">Search</button>
50 = TEXT
50.value = </form>
}
30 = TEXT
30.value = </div>
}
[end]
[traverse(page, "uid") == 52]
page.meta.robots = noindex, nofollow
[end]Fluid Template — Custom 404 Page (Fluid Handler)
File: EXT:site_package/Resources/Private/Templates/Error/404.html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true">
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>Page Not Found - Example Company</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
display: flex;
min-height: 100vh;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
}
.error-page {
text-align: center;
padding: 2rem;
max-width: 600px;
}
.error-page__code {
font-size: 6rem;
font-weight: 700;
color: #dee2e6;
line-height: 1;
}
.error-page__title {
font-size: 1.5rem;
margin: 1rem 0;
}
.error-page__message {
color: #6c757d;
margin-bottom: 2rem;
}
.error-page__link {
display: inline-block;
padding: 0.75rem 1.5rem;
background-color: #0d6efd;
color: #fff;
text-decoration: none;
border-radius: 0.25rem;
}
.error-page__link:hover {
background-color: #0b5ed7;
}
</style>
</head>
<body>
<main class="error-page">
<div class="error-page__code">404</div>
<h1 class="error-page__title">Page Not Found</h1>
<p class="error-page__message">
The page you are looking for might have been removed, had its name changed,
or is temporarily unavailable.
</p>
<p class="error-page__message">
Requested URL: <code>{url}</code>
</p>
<a href="/" class="error-page__link">Go to Homepage</a>
</main>
</body>
</html>
</html>Custom PHP Error Handler
File: EXT:site_package/Classes/Error/NotFoundHandler.php
<?php
declare(strict_types=1);
namespace Vendor\SitePackage\Error;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerInterface;
use TYPO3\CMS\Core\Http\HtmlResponse;
use TYPO3\CMS\Core\Http\RedirectResponse;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
class NotFoundHandler implements PageErrorHandlerInterface
{
private int $statusCode;
public function __construct(int $statusCode, array $configuration)
{
$this->statusCode = $statusCode;
}
public function handlePageError(
ServerRequestInterface $request,
string $message,
array $reasons = []
): ResponseInterface {
$requestedUrl = (string)$request->getUri();
// Redirect known old URLs
$redirectMap = [
'/old-about-us' => '/about/',
'/old-contact' => '/contact/',
];
$path = $request->getUri()->getPath();
if (isset($redirectMap[$path])) {
return new RedirectResponse($redirectMap[$path], 301);
}
// Render 404 template
$view = GeneralUtility::makeInstance(StandaloneView::class);
$view->setTemplatePathAndFilename(
'EXT:site_package/Resources/Private/Templates/Error/NotFound.html'
);
$view->assignMultiple([
'url' => $requestedUrl,
'statusCode' => $this->statusCode,
'message' => $message,
]);
return new HtmlResponse($view->render(), $this->statusCode);
}
}TSconfig — Hide Error Pages from Navigation
# Hide error pages in page tree navigation menus
# (alternative: set nav_hide in page properties)
TCEMAIN.table.pages.disableHideAtCopy = 0TypoScript — Exclude Error Pages from Sitemap
plugin.tx_seo.config.xmlSitemap.sitemaps.pages.config {
additionalWhere = {#no_index} = 0 AND {#uid} NOT IN (50,51,52,60,61)
}Notes
- The
Pageerror handler fetches the TYPO3 page content via an internal subrequest. This gives full TypoScript/Fluid rendering but requires the error page to be accessible. - Error code
0is the catch-all for any HTTP error not explicitly configured. - Language-specific error handling is configured within each language section of
config.yaml. This allows different error pages per language. - The
Fluiderror handler renders a standalone Fluid template without full TYPO3 page rendering. It is faster but lacks TypoScript processing. - The
PHPerror handler gives maximum flexibility (redirects, logging, custom logic) but requires custom PHP code. - Error pages should always have
noindex, nofollowmeta tags to prevent search engine indexing. - Avoid creating circular errors: the error page itself must render without errors. Do not use complex plugins or database-dependent content on error pages.
- In v12+, the site configuration error handling replaces the old
config.typolinkLinkAccessRestrictedPagesapproach for 403 errors. - Test error pages by visiting a non-existent URL. Check that the HTTP status code is actually 404 (not 200) using browser DevTools.
- For v13+, the error handling configuration remains the same in
config.yaml.
Recipe: Extbase Plugin TypoScript Configuration
Version: v12+
What this builds
Complete TypoScript configuration for an Extbase plugin including plugin settings, persistence mapping, view paths, and frontend rendering setup for a "Job Board" extension.
TypoScript — Plugin Configuration
plugin.tx_jobboard {
view {
templateRootPaths {
0 = EXT:job_board/Resources/Private/Templates/
10 = {$plugin.tx_jobboard.view.templateRootPath}
}
partialRootPaths {
0 = EXT:job_board/Resources/Private/Partials/
10 = {$plugin.tx_jobboard.view.partialRootPath}
}
layoutRootPaths {
0 = EXT:job_board/Resources/Private/Layouts/
10 = {$plugin.tx_jobboard.view.layoutRootPath}
}
}
persistence {
storagePid = {$plugin.tx_jobboard.persistence.storagePid}
recursive = 1
}
settings {
# List view
list {
itemsPerPage = 12
orderBy = publishDate
orderDirection = desc
showCategories = 1
showLocation = 1
}
# Detail view
detail {
pageUid = {$plugin.tx_jobboard.settings.detailPageUid}
showApplyButton = 1
showSocialShare = 1
}
# Category filter
categories {
# Comma-separated UIDs or leave empty for all
include =
exclude =
}
# Email notification for new applications
notification {
senderEmail = {$plugin.tx_jobboard.settings.notification.senderEmail}
senderName = {$plugin.tx_jobboard.settings.notification.senderName}
recipientEmail = {$plugin.tx_jobboard.settings.notification.recipientEmail}
templateRootPath = EXT:job_board/Resources/Private/Templates/Email/
}
# SEO
seo {
enableStructuredData = 1
defaultEmploymentType = FULL_TIME
}
}
features {
skipDefaultArguments = 1
requireCHashArgumentForActionArguments = 0
}
}TypoScript Constants
plugin.tx_jobboard {
view {
templateRootPath = EXT:job_board/Resources/Private/Templates/
partialRootPath = EXT:job_board/Resources/Private/Partials/
layoutRootPath = EXT:job_board/Resources/Private/Layouts/
}
persistence {
storagePid = 42
}
settings {
detailPageUid = 45
notification {
senderEmail = noreply@example.com
senderName = Job Board
recipientEmail = hr@example.com
}
}
}TypoScript — Persistence Mapping (v12+)
File: EXT:job_board/Configuration/Extbase/Persistence/Classes.php
<?php
declare(strict_types=1);
return [
\Vendor\JobBoard\Domain\Model\JobOffer::class => [
'tableName' => 'tx_jobboard_domain_model_joboffer',
'properties' => [
'publishDate' => [
'fieldName' => 'publish_date',
],
'employmentType' => [
'fieldName' => 'employment_type',
],
'isRemote' => [
'fieldName' => 'remote',
],
],
],
\Vendor\JobBoard\Domain\Model\Application::class => [
'tableName' => 'tx_jobboard_domain_model_application',
'properties' => [
'coverLetter' => [
'fieldName' => 'cover_letter',
],
'submittedAt' => [
'fieldName' => 'crdate',
],
],
],
\Vendor\JobBoard\Domain\Model\Category::class => [
'tableName' => 'sys_category',
],
];Plugin Registration
File: EXT:job_board/Configuration/TCA/Overrides/tt_content.php
<?php
declare(strict_types=1);
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
defined('TYPO3') or die();
ExtensionUtility::registerPlugin(
'JobBoard',
'List',
'LLL:EXT:job_board/Resources/Private/Language/locallang_be.xlf:plugin.list.title',
'ext-jobboard-list',
'plugins',
'LLL:EXT:job_board/Resources/Private/Language/locallang_be.xlf:plugin.list.description'
);
ExtensionUtility::registerPlugin(
'JobBoard',
'Detail',
'LLL:EXT:job_board/Resources/Private/Language/locallang_be.xlf:plugin.detail.title',
'ext-jobboard-detail',
'plugins',
'LLL:EXT:job_board/Resources/Private/Language/locallang_be.xlf:plugin.detail.description'
);
// FlexForm for the List plugin
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_addlist']['jobboard_list'] = 'pi_flexform';
$GLOBALS['TCA']['tt_content']['types']['list']['subtypes_excludelist']['jobboard_list'] = 'layout,select_key,pages,recursive';
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
'jobboard_list',
'FILE:EXT:job_board/Configuration/FlexForms/List.xml'
);ext_localconf.php — Controller/Action Mapping
<?php
declare(strict_types=1);
use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
use Vendor\JobBoard\Controller\JobOfferController;
defined('TYPO3') or die();
ExtensionUtility::configurePlugin(
'JobBoard',
'List',
[
JobOfferController::class => 'list, filter',
],
// Non-cacheable actions
[
JobOfferController::class => 'filter',
]
);
ExtensionUtility::configurePlugin(
'JobBoard',
'Detail',
[
JobOfferController::class => 'show, apply, confirmApplication',
],
[
JobOfferController::class => 'apply, confirmApplication',
]
);FlexForm for Plugin Settings
File: EXT:job_board/Configuration/FlexForms/List.xml
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<T3DataStructure>
<sheets>
<sDEF>
<ROOT>
<type>array</type>
<el>
<settings.list.itemsPerPage>
<label>LLL:EXT:job_board/Resources/Private/Language/locallang_be.xlf:flexform.itemsPerPage</label>
<config>
<type>input</type>
<size>5</size>
<eval>int</eval>
<default>12</default>
</config>
</settings.list.itemsPerPage>
<settings.list.orderBy>
<label>LLL:EXT:job_board/Resources/Private/Language/locallang_be.xlf:flexform.orderBy</label>
<config>
<type>select</type>
<renderType>selectSingle</renderType>
<items>
<numIndex index="0">
<label>Publish Date</label>
<value>publishDate</value>
</numIndex>
<numIndex index="1">
<label>Title</label>
<value>title</value>
</numIndex>
</items>
</config>
</settings.list.orderBy>
<settings.categories.include>
<label>LLL:EXT:job_board/Resources/Private/Language/locallang_be.xlf:flexform.categories</label>
<config>
<type>category</type>
<relationship>manyToMany</relationship>
<size>5</size>
<maxitems>20</maxitems>
</config>
</settings.categories.include>
<settings.detail.pageUid>
<label>LLL:EXT:job_board/Resources/Private/Language/locallang_be.xlf:flexform.detailPage</label>
<config>
<type>group</type>
<allowed>pages</allowed>
<size>1</size>
<maxitems>1</maxitems>
</config>
</settings.detail.pageUid>
</el>
</ROOT>
</sDEF>
</sheets>
</T3DataStructure>TypoScript — Override Template Paths per Plugin Instance
# Override templates for a specific page
[traverse(page, "uid") == 55]
plugin.tx_jobboard_list {
view {
templateRootPaths.20 = EXT:site_package/Resources/Private/Templates/JobBoard/
}
settings {
list.itemsPerPage = 6
}
}
[end]
# Global override from site package
plugin.tx_jobboard {
view {
templateRootPaths.20 = EXT:site_package/Resources/Private/Templates/JobBoard/
partialRootPaths.20 = EXT:site_package/Resources/Private/Partials/JobBoard/
}
}TypoScript — Override for tt_content Rendering
# Configure the tt_content rendering for the plugin
tt_content.list.20.jobboard_list =< plugin.tx_jobboard_list
tt_content.list.20.jobboard_detail =< plugin.tx_jobboard_detailFluid Template Example
File: EXT:job_board/Resources/Private/Templates/JobOffer/List.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">
<div class="job-list">
<f:if condition="{paginator.paginatedItems -> f:count()}">
<f:then>
<div class="job-list__items">
<f:for each="{paginator.paginatedItems}" as="job">
<article class="job-card" itemscope itemtype="https://schema.org/JobPosting">
<h2 class="job-card__title" itemprop="title">
<f:link.action action="show" controller="JobOffer"
pluginName="Detail"
pageUid="{settings.detail.pageUid}"
arguments="{jobOffer: job}">
{job.title}
</f:link.action>
</h2>
<div class="job-card__meta">
<f:if condition="{job.location}">
<span class="job-card__location" itemprop="jobLocation" itemscope itemtype="https://schema.org/Place">
<span itemprop="address">{job.location}</span>
</span>
</f:if>
<span class="job-card__type" itemprop="employmentType">
{job.employmentType}
</span>
<f:if condition="{job.isRemote}">
<span class="job-card__remote">Remote</span>
</f:if>
</div>
<p class="job-card__teaser" itemprop="description">
<f:format.crop maxCharacters="200" respectWordBoundaries="1">
{job.description -> f:format.stripTags()}
</f:format.crop>
</p>
<time class="job-card__date" datetime="{f:format.date(date: job.publishDate, format: 'Y-m-d')}" itemprop="datePosted">
<f:format.date date="{job.publishDate}" format="%d.%m.%Y" />
</time>
</article>
</f:for>
</div>
<f:comment>
Pagination: build in the controller (f:widget.paginate was removed in v12):
$paginator = new QueryResultPaginator($jobOffers, $currentPageNumber, $itemsPerPage);
$pagination = new SimplePagination($paginator);
$this->view->assignMultiple(['paginator' => $paginator, 'pagination' => $pagination]);
</f:comment>
<f:if condition="{pagination.allPageNumbers -> f:count()} > 1">
<nav class="pagination" aria-label="Pagination">
<f:for each="{pagination.allPageNumbers}" as="pageNumber">
<f:link.action action="list" arguments="{currentPageNumber: pageNumber}">{pageNumber}</f:link.action>
</f:for>
</nav>
</f:if>
</f:then>
<f:else>
<p class="job-list__empty">
<f:translate key="LLL:EXT:job_board/Resources/Private/Language/locallang.xlf:list.noResults" />
</p>
</f:else>
</f:if>
</div>
</f:section>
</html>v13+ — Site Sets Integration
File: EXT:job_board/Configuration/Sets/JobBoard/config.yaml
name: vendor/job-board
label: Job Board Extension
dependencies:
- typo3/fluid-styled-contentFile: EXT:job_board/Configuration/Sets/JobBoard/settings.definitions.yaml
settings:
job_board.persistence.storagePid:
label: 'Storage Page ID for Job Offers'
type: int
default: 0
job_board.settings.detailPageUid:
label: 'Detail Page UID'
type: int
default: 0
job_board.settings.list.itemsPerPage:
label: 'Items per Page'
type: int
default: 12File: EXT:job_board/Configuration/Sets/JobBoard/setup.typoscript
plugin.tx_jobboard {
persistence.storagePid = {$job_board.persistence.storagePid}
settings.detail.pageUid = {$job_board.settings.detailPageUid}
settings.list.itemsPerPage = {$job_board.settings.list.itemsPerPage}
}Notes
- Plugin TypoScript uses
plugin.tx_extensionname(lowercase, no underscores from vendor). The specific plugin isplugin.tx_extensionname_pluginname. - FlexForm settings from the plugin content element override TypoScript
settings.*values. This allows editors to customize per-instance. features.skipDefaultArguments = 1prevents default action/controller parameters from appearing in URLs.requireCHashArgumentForActionArguments = 0is needed when using route enhancers, as cHash validation is handled differently.- In v12+, persistence mapping is done via
Configuration/Extbase/Persistence/Classes.php, not via TypoScriptconfig.tx_extbase.persistence.classes. - Non-cacheable actions (form submissions, filtered lists) are defined in the second array of
configurePlugin(). Keep these to a minimum for performance. - Template path arrays use numeric keys. Key
0is the extension default,10is for constants/settings override,20for site package overrides. - The
f:widget.paginateViewHelper was removed in v12. Build pagination in the controller withQueryResultPaginatorandSimplePagination(TYPO3 Pagination API) and render the page links in Fluid. - In v13+ with Site Sets, constants are replaced by
settings.yamldefinitions. Reference them with{$setting.name}in TypoScript (same syntax as constants).
Recipe: IMAGE cObject and Responsive Images
Version: v12+
What this builds
Responsive image rendering using the IMAGE cObject with srcset, picture element approaches, and crop variants for art direction.
TypoScript — Basic IMAGE cObject
lib.headerImage = IMAGE
lib.headerImage {
file {
import.data = levelmedia:-1, slide
treatIdAsReference = 1
width = 1200c
height = 400c
}
altText.data = levelmedia:-1, slide
titleText.data = levelmedia:-1, slide
# Add CSS class
params = class="header-image"
# Wrap in figure element
stdWrap.wrap = <figure class="header-figure">|</figure>
}TypoScript — Responsive Images with srcset
lib.responsiveImage = IMAGE
lib.responsiveImage {
file {
import.data = file:current:uid
treatIdAsReference = 1
width = 1200
}
altText.data = file:current:alternative
titleText.data = file:current:title
# srcset for responsive images
sourceCollection {
small {
width = 400
srcsetCandidate = 400w
mediaQuery = (max-width: 480px)
dataKey = small
}
medium {
width = 800
srcsetCandidate = 800w
mediaQuery = (max-width: 768px)
dataKey = medium
}
large {
width = 1200
srcsetCandidate = 1200w
mediaQuery = (max-width: 1200px)
dataKey = large
}
xlarge {
width = 1600
srcsetCandidate = 1600w
mediaQuery = (min-width: 1201px)
dataKey = xlarge
}
}
# Output as <picture> element
layoutKey = srcset
}TypoScript — imgResource for processed file URLs
lib.processedImageUrl = IMG_RESOURCE
lib.processedImageUrl {
file {
import.data = file:current:uid
treatIdAsReference = 1
width = 600c
height = 400c
}
stdWrap.wrap = <div class="background" style="background-image: url('|');"></div>
}Fluid Template — Responsive Images with ViewHelper
File: EXT:site_package/Resources/Private/Partials/Media/ResponsiveImage.html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:comment><!-- Simple responsive image with f:image --></f:comment>
<f:image image="{file}"
width="1200"
alt="{file.alternative}"
title="{file.title}"
class="img-fluid"
loading="lazy" />
<f:comment><!-- Picture element with multiple sources for art direction --></f:comment>
<picture>
<source media="(min-width: 1200px)"
srcset="{f:uri.image(image: file, width: '1600c', height: '600c', cropVariant: 'desktop')}" />
<source media="(min-width: 768px)"
srcset="{f:uri.image(image: file, width: '1024c', height: '500c', cropVariant: 'tablet')}" />
<f:image image="{file}"
width="768c"
height="500c"
cropVariant="mobile"
alt="{file.alternative}"
loading="lazy"
class="img-fluid" />
</picture>
<f:comment><!-- srcset with sizes attribute --></f:comment>
<img srcset="{f:uri.image(image: file, width: '400')} 400w,
{f:uri.image(image: file, width: '800')} 800w,
{f:uri.image(image: file, width: '1200')} 1200w,
{f:uri.image(image: file, width: '1600')} 1600w"
sizes="(max-width: 480px) 400px,
(max-width: 768px) 800px,
(max-width: 1200px) 1200px,
1600px"
src="{f:uri.image(image: file, width: '1200')}"
alt="{file.alternative}"
loading="lazy"
class="img-fluid" />
</html>Crop Variant Configuration (TCA)
This is configured in ext_localconf.php or Configuration/TCA/Overrides/sys_file_reference.php:
$GLOBALS['TCA']['sys_file_reference']['columns']['crop']['config']['cropVariants'] = [
'desktop' => [
'title' => 'Desktop',
'allowedAspectRatios' => [
'16:9' => [
'title' => '16:9',
'value' => 16 / 9,
],
'21:9' => [
'title' => '21:9',
'value' => 21 / 9,
],
],
'selectedRatio' => '16:9',
],
'tablet' => [
'title' => 'Tablet',
'allowedAspectRatios' => [
'4:3' => [
'title' => '4:3',
'value' => 4 / 3,
],
],
'selectedRatio' => '4:3',
],
'mobile' => [
'title' => 'Mobile',
'allowedAspectRatios' => [
'1:1' => [
'title' => '1:1',
'value' => 1.0,
],
'3:4' => [
'title' => '3:4',
'value' => 3 / 4,
],
],
'selectedRatio' => '1:1',
],
];TypoScript — Content Element Image Rendering Override
# Override default image rendering for textmedia CE
tt_content.textmedia {
dataProcessing {
10 = TYPO3\CMS\Frontend\DataProcessing\FilesProcessor
10 {
references.fieldName = assets
as = files
}
}
}
# Custom image rendering configuration
lib.contentElement.settings.media {
popup {
# Disable click-enlarge
enabled = 0
}
gallery {
columnSpacing = 10
rows {
horizontal {
borderWidth = 0
borderPadding = 0
}
}
}
}Notes
- The
sourceCollectionapproach generatessrcsetor<picture>markup depending on thelayoutKeysetting. - Valid
layoutKeyvalues:default(plain<img>),srcset(img with srcset),picture(picture element),data(data-attributes for JS). treatIdAsReference = 1is required when working with FAL file references (sys_file_reference UIDs) instead of sys_file UIDs.- Crop variants define named crop areas in TCA. Reference them via
cropVariantin Fluid or TypoScript. width = 1200cmeans crop-and-scale to 1200px. Withoutc, the image is scaled proportionally. Usemfor max-width/max-height scaling.loading="lazy"is natively supported since v11. Avoid it for above-the-fold images (hero, header).- For WebP conversion, configure
$GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']to includewebpand use ImageMagick/GraphicsMagick with WebP support. - In v12+, the
FilesProcessoris the standard way to make file references available in Fluid templates.
Recipe: Navigation Menu with MenuProcessor
Version: v12+
What this builds
A multi-level navigation menu using the MenuProcessor DataProcessor with active state handling, dropdown support, and accessible markup.
TypoScript
page.10 {
dataProcessing {
# Main navigation (top-level with subpages)
10 = TYPO3\CMS\Frontend\DataProcessing\MenuProcessor
10 {
levels = 3
expandAll = 0
includeSpacer = 1
excludeUidList =
as = mainNavigation
# Exclude pages with doktype > 199 (folders, recycler)
# and pages with nav_hide = 1 (handled automatically)
}
# Footer navigation (specific page subtree)
20 = TYPO3\CMS\Frontend\DataProcessing\MenuProcessor
20 {
special = directory
special.value = 42
levels = 1
as = footerNavigation
}
# Meta navigation (explicit page list)
30 = TYPO3\CMS\Frontend\DataProcessing\MenuProcessor
30 {
special = list
special.value = 10,11,12
as = metaNavigation
}
}
}Fluid Template
File: EXT:site_package/Resources/Private/Partials/Navigation/Main.html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true">
<nav class="main-nav" aria-label="Main navigation">
<ul class="nav-list nav-list--level-1">
<f:for each="{mainNavigation}" as="item">
<li class="nav-item{f:if(condition: item.active, then: ' nav-item--active')}{f:if(condition: item.current, then: ' nav-item--current')}{f:if(condition: item.children, then: ' nav-item--has-children')}{f:if(condition: '{item.spacer}', then: ' nav-item--spacer')}">
<f:if condition="{item.spacer}">
<f:then>
<span class="nav-spacer" aria-hidden="true">{item.title}</span>
</f:then>
<f:else>
<a href="{item.link}"
class="nav-link{f:if(condition: item.active, then: ' nav-link--active')}"
{f:if(condition: item.target, then: 'target="{item.target}"')}
{f:if(condition: item.current, then: 'aria-current="page"')}>
{item.title}
</a>
<f:if condition="{item.children}">
<f:render partial="Navigation/MainSubmenu"
arguments="{children: item.children, level: 2}" />
</f:if>
</f:else>
</f:if>
</li>
</f:for>
</ul>
</nav>
</html>File: EXT:site_package/Resources/Private/Partials/Navigation/MainSubmenu.html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true">
<ul class="nav-list nav-list--level-{level}">
<f:for each="{children}" as="child">
<li class="nav-item{f:if(condition: child.active, then: ' nav-item--active')}{f:if(condition: child.current, then: ' nav-item--current')}{f:if(condition: child.children, then: ' nav-item--has-children')}">
<a href="{child.link}"
class="nav-link"
{f:if(condition: child.target, then: 'target="{child.target}"')}
{f:if(condition: child.current, then: 'aria-current="page"')}>
{child.title}
</a>
<f:if condition="{child.children}">
<f:render partial="Navigation/MainSubmenu"
arguments="{children: child.children, level: '{f:math.sum(a: level, b: 1)}'}" />
</f:if>
</li>
</f:for>
</ul>
</html>File: EXT:site_package/Resources/Private/Partials/Navigation/Footer.html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true">
<nav class="footer-nav" aria-label="Footer navigation">
<ul class="footer-nav__list">
<f:for each="{footerNavigation}" as="item">
<li class="footer-nav__item">
<a href="{item.link}" class="footer-nav__link">{item.title}</a>
</li>
</f:for>
</ul>
</nav>
</html>Alternative: HMENU (classic approach)
lib.mainNavigation = HMENU
lib.mainNavigation {
1 = TMENU
1 {
NO {
wrapItemAndSub = <li class="nav-item">|</li>
ATagTitle.field = nav_title // title
stdWrap.htmlSpecialChars = 1
}
ACT = 1
ACT {
wrapItemAndSub = <li class="nav-item nav-item--active">|</li>
ATagTitle.field = nav_title // title
stdWrap.htmlSpecialChars = 1
}
CUR = 1
CUR {
wrapItemAndSub = <li class="nav-item nav-item--current" aria-current="page">|</li>
ATagTitle.field = nav_title // title
stdWrap.htmlSpecialChars = 1
}
wrap = <ul class="nav-list nav-list--level-1">|</ul>
}
2 = TMENU
2 {
NO {
wrapItemAndSub = <li class="nav-item">|</li>
stdWrap.htmlSpecialChars = 1
}
ACT = 1
ACT {
wrapItemAndSub = <li class="nav-item nav-item--active">|</li>
stdWrap.htmlSpecialChars = 1
}
wrap = <ul class="nav-list nav-list--level-2">|</ul>
}
}Notes
MenuProcessoris the recommended approach since v9+. It provides a clean data array for Fluid templates.- The
activeproperty istruefor all pages in the current rootline. Thecurrentproperty istrueonly for the current page. expandAll = 0(default) only expands submenus for pages in the active rootline. Set to1for mega-menus.includeSpacer = 1includes pages with doktype 199 (spacer). Handle them in Fluid with{item.spacer}.- Use
nav_title // titlefallback to prefer the navigation title over the page title. - The recursive partial approach (
MainSubmenucalling itself) handles unlimited nesting depth cleanly. - For the classic HMENU approach, states are:
NO(normal),ACT(active/in rootline),CUR(current page),IFSUB(has submenu),ACTIFSUB(active with submenu). - In v13+, the
MenuProcessordata structure remains the same, so Fluid templates are forward-compatible.
Recipe: Multi-Language Setup
Version: v12+
What this builds
A complete multi-language TYPO3 setup with language-dependent configuration, hreflang tags for SEO, and a language switcher menu.
Site Configuration (config/sites/main/config.yaml)
base: 'https://www.example.com/'
rootPageId: 1
languages:
-
title: English
enabled: true
languageId: 0
base: /
typo3Language: default
locale: en_US.UTF-8
iso-two-letter-iso-code: en
navigationTitle: EN
hreflang: en-US
direction: ltr
flag: us
websiteTitle: 'Example Company'
-
title: Deutsch
enabled: true
languageId: 1
base: /de/
typo3Language: de
locale: de_DE.UTF-8
iso-two-letter-iso-code: de
navigationTitle: DE
hreflang: de-DE
direction: ltr
flag: de
websiteTitle: 'Beispiel Firma'
fallbackType: strict
-
title: 'Francais'
enabled: true
languageId: 2
base: /fr/
typo3Language: fr
locale: fr_FR.UTF-8
iso-two-letter-iso-code: fr
navigationTitle: FR
hreflang: fr-FR
direction: ltr
flag: fr
websiteTitle: 'Exemple Entreprise'
fallbackType: fallback
fallbacks: '1,0'
errorHandling:
-
errorCode: 404
errorHandler: Page
errorContentSource: 't3://page?uid=10'TypoScript — Language-Dependent Configuration
# Base configuration
config {
sys_language_uid = 0
language = en
locale_all = en_US.UTF-8
htmlTag_langKey = en
}
# Language-specific overrides using conditions
[siteLanguage("languageId") == 1]
config {
sys_language_uid = 1
language = de
locale_all = de_DE.UTF-8
htmlTag_langKey = de
}
# German-specific date format
lib.dateFormat = TEXT
lib.dateFormat.strftime = %d. %B %Y
[end]
[siteLanguage("languageId") == 2]
config {
sys_language_uid = 2
language = fr
locale_all = fr_FR.UTF-8
htmlTag_langKey = fr
}
lib.dateFormat = TEXT
lib.dateFormat.strftime = %d %B %Y
[end]TypoScript — Hreflang Tags
# Automatic hreflang tags in page header
page.headerData {
100 = HMENU
100 {
special = language
special.value = 0,1,2
special.normalWhenNoLanguage = 0
1 = TMENU
1 {
NO = 1
NO {
stdWrap.cObject = COA
stdWrap.cObject {
10 = LOAD_REGISTER
10 {
languageHreflang.cObject = TEXT
languageHreflang.cObject {
value = en-US
override = de-DE
override.if.value = 1
override.if.equals.data = register:sys_language_uid
override = fr-FR
override.if.value = 2
override.if.equals.data = register:sys_language_uid
}
}
20 = TEXT
20 {
typolink {
parameter.data = page:uid
additionalParams.data = register:sys_language_uid
additionalParams.wrap = &L=|
returnLast = url
forceAbsoluteUrl = 1
}
wrap = <link rel="alternate" hreflang="{register:languageHreflang}" href="|" />
insertData = 1
}
}
doNotLinkIt = 1
}
}
}
}Simpler Hreflang via LanguageMenuProcessor + Fluid (recommended)
page.10 {
dataProcessing {
50 = TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor
50 {
languages = auto
as = languageMenu
}
}
}File: EXT:site_package/Resources/Private/Partials/Meta/Hreflang.html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true">
<f:section name="Hreflang">
<f:for each="{languageMenu}" as="language">
<f:if condition="{language.available}">
<link rel="alternate" hreflang="{language.hreflang}" href="{language.link}" />
</f:if>
</f:for>
<f:comment><!-- x-default points to the default language --></f:comment>
<f:for each="{languageMenu}" as="language">
<f:if condition="{language.languageId} == 0">
<f:if condition="{language.available}">
<link rel="alternate" hreflang="x-default" href="{language.link}" />
</f:if>
</f:if>
</f:for>
</f:section>
</html>Add to the page <head> section:
<f:render partial="Meta/Hreflang" section="Hreflang" arguments="{languageMenu: languageMenu}" />Fluid Template — Language Switcher
File: EXT:site_package/Resources/Private/Partials/Navigation/Language.html
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true">
<nav class="language-nav" aria-label="Language selection">
<ul class="language-nav__list">
<f:for each="{languageMenu}" as="language">
<li class="language-nav__item{f:if(condition: language.active, then: ' language-nav__item--active')}{f:if(condition: '!{language.available}', then: ' language-nav__item--unavailable')}">
<f:if condition="{language.available}">
<f:then>
<a href="{language.link}"
hreflang="{language.hreflang}"
class="language-nav__link"
{f:if(condition: language.active, then: 'aria-current="true"')}
title="{language.title}">
{language.navigationTitle}
</a>
</f:then>
<f:else>
<span class="language-nav__link language-nav__link--disabled"
title="{language.title} - not available">
{language.navigationTitle}
</span>
</f:else>
</f:if>
</li>
</f:for>
</ul>
</nav>
</html>TypoScript — Language-Specific Content
# Different footer text per language
lib.footerText = TEXT
lib.footerText {
value = © {date:U} Example Company. All rights reserved.
insertData = 1
strftime = %Y
}
[siteLanguage("languageId") == 1]
lib.footerText.value = © {date:U} Beispiel Firma. Alle Rechte vorbehalten.
[end]
[siteLanguage("languageId") == 2]
lib.footerText.value = © {date:U} Exemple Entreprise. Tous droits reserves.
[end]
# Language-aware link to a specific page
lib.imprintLink = TEXT
lib.imprintLink {
typolink {
parameter = 15
# L parameter is set automatically based on site configuration
}
value = Imprint
}
[siteLanguage("languageId") == 1]
lib.imprintLink.value = Impressum
[end]Notes
- Site configuration (
config.yaml) is the primary place for language setup since v9+. TypoScriptconfig.sys_language_*settings should match the site config but are increasingly handled automatically. fallbackTypeoptions:strict(only show translated content),fallback(fall back to specified languages),free(free content mode).- The
LanguageMenuProcessoris the recommended way to build language switchers. It respects page availability per language. languages = autoin the LanguageMenuProcessor includes all languages defined in the site configuration.- Hreflang tags should include
x-defaultpointing to the primary language for search engines. - In v12+, the
siteLanguage()condition function replaces the old[globalVar = GP:L = 1]condition syntax. - Always use
typolinkfor links — it automatically generates the correct language prefix based on the site configuration. - In v13+, language configuration is part of Site Sets and can be managed through
settings.yaml.
Recipe: Rich Text Editor Configuration (CKEditor)
Version: v12+
What this builds
Page TSconfig configuration for CKEditor presets, controlling allowed HTML tags, heading levels, format options, and custom styles in the TYPO3 Rich Text Editor.
TSconfig — Custom RTE Preset Registration
File: EXT:site_package/Configuration/RTE/SitePackage.yaml
imports:
- { resource: "EXT:rte_ckeditor/Configuration/RTE/Default.yaml" }
editor:
config:
# Toolbar configuration
toolbar:
items:
- bold
- italic
- '|'
- bulletedList
- numberedList
- '|'
- indent
- outdent
- '|'
- blockQuote
- link
- '|'
- insertTable
- '|'
- heading
- style
- '|'
- sourceEditing
- removeFormat
- '|'
- undo
- redo
# Heading levels
heading:
options:
- { model: 'paragraph', title: 'Paragraph' }
- { model: 'heading2', view: 'h2', title: 'Heading 2' }
- { model: 'heading3', view: 'h3', title: 'Heading 3' }
- { model: 'heading4', view: 'h4', title: 'Heading 4' }
# Custom styles dropdown
style:
definitions:
- { name: 'Lead text', element: 'p', classes: ['text-lead'] }
- { name: 'Small text', element: 'p', classes: ['text-small'] }
- { name: 'Highlight', element: 'span', classes: ['text-highlight'] }
- { name: 'Button primary', element: 'a', classes: ['btn', 'btn-primary'] }
- { name: 'Button secondary', element: 'a', classes: ['btn', 'btn-secondary'] }
# Link configuration
link:
decorators:
openInNewTab:
mode: 'manual'
label: 'Open in new tab'
defaultValue: false
attributes:
target: '_blank'
rel: 'noopener noreferrer'
# Table configuration
table:
contentToolbar:
- tableColumn
- tableRow
- mergeTableCells
- tableProperties
- tableCellProperties
# Word count (optional)
wordCount:
displayCharacters: true
displayWords: true
# External plugins (if needed)
externalPlugins: {}
processing:
# Allowed HTML tags
allowTags:
- a
- abbr
- b
- blockquote
- br
- caption
- cite
- code
- em
- figcaption
- figure
- h2
- h3
- h4
- hr
- i
- img
- li
- ol
- p
- pre
- span
- strong
- sub
- sup
- table
- tbody
- td
- th
- thead
- tr
- ul
# Allowed tag attributes
allowAttributes:
- class
- href
- target
- rel
- id
- colspan
- rowspan
- src
- alt
- title
# Deny specific tags
denyTags:
- font
- center
- div
- h1
- h5
- h6TSconfig — Assign RTE Preset
File: EXT:site_package/Configuration/page.tsconfig or via Page TSconfig field:
# Assign the custom preset globally
RTE.default.preset = SitePackage
# Override preset for specific fields
RTE.config.tt_content.bodytext.preset = SitePackage
# Minimal preset for header fields (if using RTE in headers)
RTE.config.tt_content.subheader.preset = minimal
# Disable RTE for specific content types
RTE.config.tt_content.bodytext.types.header.disabled = 1TSconfig — Restrict RTE Features per Content Type
# Simplified RTE for news teaser text
RTE.config.tx_news_domain_model_news.bodytext.preset = minimal
# Override for specific page trees (use conditions or set on page)
[page["uid"] in [42,43,44]]
RTE.default.preset = minimal
[end]Register the Preset in ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['RTE']['Presets']['SitePackage'] =
'EXT:site_package/Configuration/RTE/SitePackage.yaml';v13+ — Register via Configuration/Sets (Site Sets)
File: EXT:site_package/Configuration/Sets/SitePackage/config.yaml
name: vendor/site-package
label: Site PackageFile: EXT:site_package/Configuration/Sets/SitePackage/page.tsconfig
RTE.default.preset = SitePackageMinimal Preset Example
File: EXT:site_package/Configuration/RTE/Minimal.yaml
imports:
- { resource: "EXT:rte_ckeditor/Configuration/RTE/Default.yaml" }
editor:
config:
toolbar:
items:
- bold
- italic
- '|'
- link
- '|'
- bulletedList
- numberedList
heading:
options:
- { model: 'paragraph', title: 'Paragraph' }
removePlugins:
- table
- blockQuote
- indent
processing:
allowTags:
- a
- b
- br
- em
- li
- ol
- p
- span
- strong
- ulCustom CSS for RTE Editor Content
File: EXT:site_package/Configuration/RTE/SitePackage.yaml (add to editor section):
editor:
config:
# Load custom CSS into the editor iframe
contentsCss:
- "EXT:site_package/Resources/Public/Css/rte-editor.css"File: EXT:site_package/Resources/Public/Css/rte-editor.css
.text-lead {
font-size: 1.25em;
font-weight: 300;
line-height: 1.6;
}
.text-small {
font-size: 0.875em;
}
.text-highlight {
background-color: #fff3cd;
padding: 0.125em 0.25em;
border-radius: 0.125rem;
}
.btn {
display: inline-block;
padding: 0.5em 1em;
text-decoration: none;
border-radius: 0.25rem;
cursor: pointer;
}
.btn-primary {
background-color: #0d6efd;
color: #ffffff;
}
.btn-secondary {
background-color: #6c757d;
color: #ffffff;
}Notes
- TYPO3 v12+ uses CKEditor 5. The configuration format changed significantly from CKEditor 4 (v11 and earlier).
- The preset YAML file must be registered in
ext_localconf.phpvia$GLOBALS['TYPO3_CONF_VARS']['RTE']['Presets']. processing.allowTagscontrols which HTML tags are stored in the database. Tags not listed are stripped on save.- Custom styles need matching CSS both in the RTE editor (
contentsCss) and on the frontend. - Deny
h1in the RTE —h1should come from the page title, not from content elements. - In v13+, preset assignment can be done via Site Sets
page.tsconfiginstead of manualext_localconf.phpregistration. - The
sourceEditingtoolbar item allows editors to see raw HTML. Only enable for trusted users. removePluginsin the minimal preset disables CKEditor plugins entirely, not just toolbar buttons.
{
"typo3_to_docs": {
"12": { "typoscript": "12.4", "fluid": "2.12.0", "viewhelpers": "12.4", "coreapi": "12.4" },
"13": { "typoscript": "13.4", "fluid": "4.6.1", "viewhelpers": "13.4", "coreapi": "13.4" },
"14": { "typoscript": "14.3", "fluid": "5.3.1", "viewhelpers": "14.3", "coreapi": "14.3" }
},
"github_repos": {
"typoscript": "TYPO3-Documentation/TYPO3CMS-Reference-Typoscript",
"fluid": "TYPO3/Fluid",
"viewhelpers": "TYPO3-Documentation/TYPO3CMS-Reference-ViewHelper",
"coreapi": "TYPO3-Documentation/TYPO3CMS-Reference-CoreApi"
},
"docs_base_urls": {
"typoscript": "https://docs.typo3.org/m/typo3/reference-typoscript",
"fluid": "https://docs.typo3.org/other/typo3fluid/fluid",
"viewhelpers": "https://docs.typo3.org/other/typo3/view-helper-reference",
"coreapi": "https://docs.typo3.org/m/typo3/reference-coreapi"
},
"fallback_version": "main"
}