Files
t3bootstrap-upgrade-skills/typo3-t3bootstrap-live-design-parity/SKILL.md
T
melnicenko 53be3ef898 design-parity: RTE field rendered escaped by custom template (literal <p>)
Replace the fragile "store raw bodytext only" workaround with the durable
fix: when a custom Fluid partial renders an RTE field (enableRichtext=true,
e.g. tx_heroitem.bodytext), the backend RTE->DB transform wraps it in
<p>…</p> on every editor save, and a bare {field} / f:format.nl2br render
escapes it -> literal <p> on the page. Render RTE fields with f:format.html.
Add the general rule (check TCA enableRichtext; sweep your own/overridden
partials for nl2br/bare RTE output) and note core templates already do this.
Bump to v1.2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 11:44:31 +02:00

19 KiB

name, description, metadata
name description metadata
typo3-t3bootstrap-live-design-parity Match a TYPO3 v14 t3bootstrap-based build (local DDEV) to a reference LIVE site page by page — the frontend/design phase after a v11->v14 upgrade. Use when making an upgraded TYPO3 site look like its old/live counterpart, when doing per-page visual parity against a reference URL, when content that exists in the DB isn't rendering after a v11->v14 migration, or when customizing a t3bootstrap sitepackage's templates/SCSS/settings. Covers: the Playwright compare loop (computed style + CSS rule), the settings-vs-SCSS decision, recurring structural fixes (stranded colPos, missing templateLayout partials, template partialRootPaths precedence/shadowing, on-demand component CSS, unregistered CTypes), a full visual-parity checklist (layout/width, typography, colors, links/buttons, equal-sizing, icons, borders, footer, header/nav, hero carousel), and cache-safe techniques (full-bleed bands, image-border restyle, responsive crop variants, client-side randomisation, FAL-via-DataHandler). Assumes the site already boots on v14 (see the v11->v14 upgrade skill).
author version
Wappler 1.2

TYPO3 v14 t3bootstrap — live-design parity

Make a v14 t3bootstrap-based DDEV build look like a reference live site (usually the old v11 site being replaced), page by page. There are two layers, in order:

  1. Structural — the right content actually renders (migration artifacts often hide it).
  2. Visual parity — typography/colors/spacing/components match the reference.

Compare with Playwright ([headed, in the user's visible window]); read computed values, don't eyeball. Brand tokens (colors, fonts, page IDs, label text) are site-specific — fill the placeholders below from the site you're matching; keep a per-project log of what you changed.

How the v14 t3bootstrap theme works (resolve these first)

  • TypoScript via site sets, not sys_template. The PAGE object comes from the theme set, declared under dependencies: in config/sites/<SITE_ID>/config.yaml. (Missing → "No page configured for type=0".) flux/vhs are gone in v14; content is t3bs_* container CTypes.
  • Sitepackage <TEMPLATE_PACKAGE> (extension key <TEMPLATE_EXTKEY>), usually a fork of t3bootstrap/template, often installed from a dev-<branch> git checkout in vendor/. Content-element overrides live in EXT:<TEMPLATE_EXTKEY>/Resources/Private/Extensions/ fluid_styled_content/{Templates,Partials,Layouts}; per-CType TypoScript auto-imports from Configuration/TypoScript/Lib/ContentElement/*.typoscript.
  • ⚠ Workflow: edits in a dev-<branch> vendor checkout must be committed & pushed to the template repo before any composer update <TEMPLATE_PACKAGE> (else lost). Code changes (templates/SCSS/TypoScript/tsconfig/JS in the sitepackage) deploy via composer update. Content changes (tt_content, settings.yaml, DB) are project-local and travel via a DB dump, NOT the repo.

Per-page compare loop

  1. Open live https://<LIVE_HOST>/<path> and DDEV https://<DDEV_HOST>/<path> in Playwright. Cache-bust DDEV with ?cb=N (browser caches); ddev exec vendor/bin/typo3 cache:flush after config/SCSS/template changes (a hard flush — also rm -rf var/cache/* — for template/TS path or on-demand-CSS changes). After editing a Fluid partial/template, cache:flush alone can still serve the old render → also clear var/cache/code/fluid_template/* + var/cache/data/pages/*.
    • ?cb=N busts the PAGE HTML but NOT the compiled-CSS asset URL. Playwright caches main.css across navigations, so getComputedStyle may read stale styles after a recompile (a rule "missing" from the CSSOM though it's on disk). Verify against server truth: in-page fetch(cssUrl,{cache:'no-store'}) and grep the rule, or grep the compiled file on disk; don't trust the cached DOM. Same for server-rendered HTML — fetch(url,{cache:'no-store'}) to confirm a template change actually took, rather than the cached page.
  2. With browser_evaluate, capture the real element's computed style AND its explicit CSS rule (getComputedStyle + walk document.styleSheets for the selector). Don't assume inheritance.
  3. Diff vs DDEV → decide setting vs SCSS (below) → apply → flush → re-screenshot → verify.

Settings vs SCSS decision

  • Setting (config/sites/<SITE_ID>/settings.yaml) for anything mapped in the theme's Configuration/TypoScript/Plugin/Setup/tx_wsscss.typoscript: design.color.{primary,secondary, body-color,headings-color,header-bg,page-bg,footer-bg,footer-color,footer-headline-color,…}, design.font.{font-family-base,headings-font-family,font-size-base,h1..h6-font-size}, plus header.logo, footer.logo, meta.favicon. These are injected as SCSS vars, so editing _variables.scss for colors/fonts has NO effect — settings win.
  • SCSS (a custom partial @imported last in base-layout.scss; sizes in rem) for everything with no setting: utility classes, component tweaks, link/hover colors, icon/logo sizing, equal-sizing.
  • Webfonts: no setting → add the Google-Fonts URL to page.includeCSS in the theme's HTML/Page/head.typoscript; a font-downloader ext localizes it.
  • Vanilla-JS components: register in page.includeJSFooter (theme head.typoscript), file under Resources/Public/JavaScript/components/.

Layer 1 — Structural fixes (content that won't render). Do these FIRST.

These are v11→v14 migration artifacts, not design; they recur across many pages.

  • Stranded colPos=1 (the #1 recurring bug). v11 content sitting at colPos 1 (or 2/3) is not rendered by the v14 backend layout → the page looks half-empty though the content is in the DB. Find it site-wide: SELECT c.pid,p.title,COUNT(*) FROM tt_content c JOIN pages p ON p.uid=c.pid WHERE c.colPos=1 AND c.hidden=0 AND c.deleted=0 AND p.doktype=1 GROUP BY c.pid; (also check colPos NOT IN the known container values 2110xx). Fix: UPDATE tt_content SET colPos=0, sorting=sorting+<OFFSET> WHERE pid=<pid> AND colPos=1 AND deleted=0 — offset so moved rows sort AFTER the colPos-0 intro. Add a band class via element_classes where the live section has a background.
  • Missing templateLayout partial — "Partial Layouts/List/ not found" (news) / "List/ not found" (address). A v11 custom templateLayout (e.g. List, Jobs, Lightbox) has no v14 partial. Fix: port the partial into the sitepackage's news/address partialRootPaths dir AND register the layout in page TSconfig (tx_news.templateLayouts { X = X } / tx_address.templateLayouts). Empty-list "no results" message can be suppressed by overriding the list template for that layout.
  • Template partial PRECEDENCE / shadowing. Fluid searches partialRootPaths highest-index first. Extensions register at different indices (e.g. t3bootstrap_news at .13, your sitepackage at .12) → the higher one shadows yours, so your override is dead code (symptom: your template edits don't show; a partial you didn't write renders). Fix: register the sitepackage's paths at a higher index (e.g. .20), and base your override on the partial that actually renders (diff it against the reference — don't rewrite from a different base).
  • On-demand component CSS loads AFTER main.css. scssComponents:require key="…" (accordion, pagination, carousel, …) injects its stylesheet after your compiled main.css, so equal-specificity overrides lose. Use higher specificity or !important, or a direct target (e.g. recolour an accordion chevron via ::after { background-image: … }, not the CSS var).
  • Unregistered custom CType → FE fatal (empty-templateName InvalidTemplateResourceException). Re-register the CType for v14: TCA override (addRecordType/addTCAcolumns, guard with defined('TYPO3')), tt_content.<ctype> =< lib.contentElement + templateName in Lib/ContentElement/*.typoscript, and the FSC template/partial. If the old site used the native t3b_core rendering (e.g. its Icon.html icon-tile partial that parses path;class;identifier), reuse that instead of porting old code.
  • Content ≠ template. If a page's content differs from the reference, first check whether it's an editor-managed content divergence (the dump predates live edits) rather than a template bug — don't "fix" the template to reproduce editor content.

Layer 2 — Visual-parity checklist (run ALL for each page/section)

  • Layout / width: full-bleed section background (viewport width) with content contained (~container width). full_width=1 wrongly makes the content full-width too → keep full_width=0 and bleed only the bg in SCSS: .<band>{box-shadow:0 0 0 100vw <color>;clip-path:inset(0 -100vw)}.
  • Typography (rem): font family + base size/line-height; heading sizes; card/teaser titles (v14 often defaults bigger/bolder than the old design); teaser text size/weight. The theme's base rule is h1..h6{font-weight:500}; live is often 400 → override globally in your SCSS (equal specificity, appended later → wins). Page-title spacing: every frame carries t3b-pt-l/t3b-pb-l (~24px) padding, so a page title gets doubled gaps above/below vs live's ce-header-margin model. Scope to .page-main .page-content > .frame-type-header:first-child: margin-top for the gap ABOVE (breadcrumb→title); padding-bottom:0 + & + .frame{padding-top:0} for the gap BELOW.
  • Colors (settings first): primary/headings/nav/links, body text, section-band bg, footer bg + labels. Read the element's explicit rule (inheritance assumptions bite).
  • Links & buttons: underline on/off (match live); base + exact hover colors (links→accent, buttons→accent bg — read the :hover rule). Match the button base style (live .btn-primary is often an outline look: white bg + brand border/text, not filled). Buttons are frequently square — no border-radius on some brands (global .btn{border-radius:0}). A "read more"/CTA may be a plain link + leading chevron (::before "\203A") on live vs a styled button in v14.
  • Repeated items (cards/teasers/logos/icon tiles): live usually forces equal height AND width across the row (height:100% in an equal-height row; images object-fit:cover + fixed height; logo boxes equal size). DDEV often renders ragged — measure siblings, don't assume.
  • background-media hero image (textmedia with asBackground, usually empty text — e.g. section landing/"Auftraggeber" pages): the image is a CSS background on a grid column sitting on a grey band. Live renders it flush + container-width; DDEV insets it (frame t3b-pt-l/pb-l padding + a .bg-lightgray full-bleed). Scope .frame-type-textmedia:has(.bg-column.col-type-media): zero the frame padding, drop the full-bleed and move the grey onto > .container, media col padding-left:0, inner > div{height:100%; background-size:cover} (+ a min-height so an empty-text row still shows).
  • Images / borders: the imageborder field renders Bootstrap's padded grey .img-thumbnail; restyle to the brand look (border-color:<c>;border-radius:0;padding:0;background:transparent). Migrated imageborder/border flags may be inverted or on the wrong elements — verify against live and fix in the DB. Divider (hr.ce-div) style, footer top border, header separator, decorative brand SVG shapes (stripes/polygons) — port from the old partial.
  • Icons: match the live pixel size (icon box + svg in rem); center (text-align:center + margin-inline:auto); hover → accent.
  • Structural extras (new-but-not-old): remove v14 extras the old site lacks (empty .card-footer/news-meta, wrappers, unwanted pagination) — remove from the DOM (<f:if>), not display:none. Note Bootstrap utilities (d-none/d-sm-block) are !important → out-specify or remove the markup.
  • Footer: columns via footer.contentPageId; meta nav via navigation.footer.enabled + folderPageId; disable credits (footer.creditsEnabled:false + gate the Copyright partial). The .layout-full .page-footer rule may out-specify a plain .page-footer — match its specificity. Footer link columns may be migrated as RTE frame-type-text (not menu elements) → target the text links, excluding mailto:/tel:, when adding chevrons.
  • Header / navigation: meta menu = navigation.meta.enabled + folderPageId; nav alignment/logo = header.logoPosition. Top-level shortcut items may need to be toggle-only (href="#") if the live nav doesn't navigate on them. navigation.main.menuType is GLOBAL — a per-item mega/dropdown mix + the mega's grid/width/padding needs SCSS work (smartmenus writes inline styles on open → !important). Uppercase mega headers: text-transform:none to get title-case. A persistent green/ highlighted background on the current-page item + section header in the mega/dropdowns is --bs-dropdown-link-active-bg — set it transparent (scope to the mega) to remove the "it stays highlighted" effect while keeping --bs-dropdown-link-hover-bg for transient hover.
  • Hero / carousel (hero-item): the ext injects its own Hero.html at a fixed page.5 partialRootPaths index → re-assert your partial at a HIGHER index in page.typoscript, or your edits don't show. .carousel-fade = crossfade vs .slide = slide. The .carousel-indicators strip is a full-width high-z-index bar that steals hover from content beneath — reposition/shrink or pointer-events:none. Contain-vs-full-bleed per design.
  • Translucent overlays — paint ONE shape: a translucent box + a separate translucent shape beside it can never be seamless (hairline gap or doubled-alpha line). Merge into one element with one clip-path polygon + one rgba().
  • i18n: vendor/bin/typo3 language:update (composer mode ships no packs → English fallback).
  • Verify: flush + screenshot with ?cb=N; confirm by reading computed values.

Cache-safe & content techniques

  • Randomise/shuffle on every reload → client-side JS. The FE page is cached, so a server-side shuffle (ORDER BY RAND) freezes one order per cache lifetime. Do it in the browser. To avoid a flash, run an inline <script> right after the markup (before Bootstrap's DOMContentLoaded auto-init) rather than a footer component. Check CSP first (curl -D- for a content-security-policy header) — if enforced, inline scripts need a nonce; else prefer an external component. (Same technique for a shuffled team/address grid.)
  • Responsive per-device image cropping (hero bg): the bg renders via t3bc:backgroundImage useBootstrapCropVariants=true, which reads a per-breakpoint cropVariant NAME from settings.breakpoints (xs/sm→s576, md→s768, lg→s992, xl→s1200, xxl→s1400). To let editors crop per device, add cropVariants with those exact keys (page TSconfig TCEFORM.tt_content.<falField>.config.overrideChildTca.columns.crop.config.cropVariants — page TSconfig avoids TCA load-order issues). Base recommended aspect ratios on the element's measured rendered proportions per breakpoint (measure with Playwright + browser_resize), keep a "Frei" (free) option.
  • Bulk-create content elements + FAL references via a bootstrapped CLI PHP script (SystemEnviron- mentBuilder + Bootstrap::init + CommandLineUserAuthentication + DataHandler). Gotcha: creating a type=file FAL relation via DataHandler datamap creates the sys_file_reference but leaves uid_foreign=0 (child not wired to parent) and the parent counter 0 → after process_datamap(), read $dh->substNEWwithIDs[...] and directly UPDATE the ref's uid_foreign + set the parent file-count column = 1, then referenceindex:update. Import images with $storage->addFile(…, DuplicationBehavior::REPLACE) (or $storage->getFile('/path') to index an already-copied file).
    • Single-element clone WITHOUT DataHandler (simplest for "add one slide/CE like an existing one"): clone the sibling tt_content row and its sys_file_reference via ConnectionPool (unset uid, repoint the ref's uid_local/uid_foreign) — the copied file-count column stays correct, so it sidesteps the uid_foreign=0 gotcha entirely.
    • RTE field rendered escaped by a custom template → literal <p> on the page. Symptom: after an editor edits+saves an element (e.g. a hero slide — even just its title), its rich-text field shows literal <p>…</p> in the FE. Cause: the field is RTE (enableRichtext=true in TCA — e.g. tx_heroitem.bodytext in t3bootstrap/hero-item), so the backend's RTE→DB transform wraps the value in <p>…</p> on every save, but the custom Fluid partial renders it as ESCAPED plain text (<p>{…bodytext -> f:format.nl2br()}</p> or a bare {…}) → the tags are escaped and shown. FIX = render it as the RTE HTML it is: {record.data.<field> -> f:format.html()} (lib.parseFunc_RTE), and drop the manual <p>/nl2br. This renders editor-saved <p> correctly AND wraps raw (script-imported) text in a paragraph — so no per-slide data cleanup. (A bulk-import script that stores RAW text into an RTE field hides this until the first editor save — don't rely on that; fix the template.) General rule: whenever a custom template renders a field, check the field's TCA enableRichtext — if true, use f:format.html, never bare {field} or f:format.nl2br (both escape). Core/t3b_core templates already do this (e.g. image caption text_visible, news teaser), so the risk is only in your own/overridden partials — sweep them for nl2br/bare RTE-field output.
    • Applying customer copy across DDEV+staging: key the UPDATE on a stable identifier (e.g. the bg image filename via tt_content→sys_file_reference→sys_file), since row uids differ per env.

Deploy the design changes

  • Code (templates/SCSS/TS/tsconfig/JS): commit + push the sitepackage branch, then on the target run composer update <TEMPLATE_PACKAGE> (NOT plain composer install — that keeps the stale locked ref) + hard cache flush so SCSS recompiles. ⚠ ws_scss writes the compiled CSS in a <theme>/ subdir (public/typo3temp/assets/css/<theme>/main.css), so rm css/*.css (non-recursive) MISSES it and ships a stale main.css — remove css/<theme>/main.css (or the whole css/ tree). It recompiles on the first frontend hit (behind basic-auth, that's the user's browser reload).
  • Content (tt_content/settings/DB): travels via a DB dump or replay the same DB edits; keep them env-agnostic (e.g. key UPDATEs on stable identifiers like file names, since uids differ per env).