--- name: typo3-t3bootstrap-live-design-parity description: "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, the t3b_core spacing system (spacing.yaml / tx_t3b_spacing) and why spacing must never be fixed in SCSS, 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)." metadata: author: Wappler version: "1.3" --- # 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//config.yaml`. (Missing → "No page configured for type=0".) flux/vhs are gone in v14; content is `t3bs_*` container CTypes. - **Sitepackage** `` (extension key ``), usually a fork of `t3bootstrap/template`, often installed from a `dev-` git checkout in `vendor/`. Content-element overrides live in `EXT:/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-` vendor checkout must be **committed & pushed to the template repo** before any `composer update ` (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:///` and DDEV `https:///` 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//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 `@import`ed **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/`. --- ## Spacing (margin/padding) — a THIRD place, and NEVER your SCSS partial `t3b_core` ships an **editor-facing** spacing system. Writing a spacing fix into your custom SCSS silently disables it for that element — this is the single easiest way to break the backend field without any error appearing. **Where spacing is resolved**, weakest → strongest: 1. `EXT:t3b_core/Configuration/Yaml/Spacing.yaml` — the steps (`none/xs/s/m/l/xl/xxl`, rem values) and global defaults (typically `padding_top`/`padding_bottom` = `l`). 2. `config/sites//spacing.yaml` — per site, itself a cascade: `defaults:` → `first:` / `last:` (position in the column) → `ctypes.:` → `containers.:` (each of the latter two may carry its own `defaults/first/last`). Field keys: `padding_{top,right,bottom,left}`, `margin_{top,bottom}`. Values: a step key, or a per-breakpoint map. There is also a backend module that writes this file. 3. `tt_content.tx_t3b_spacing` — per element, TCA `type: json`, rendered as the "Spacing/Abstände" box-model palette: `{"padding_top":"none"}` or `{"padding_top":{"xs":"none","md":"l"}}`. All of it ends up as `t3b-{p,m}{t,r,b,l}-` utility classes on the frame element. - **THE TRAP.** A rule such as `.page-content > .frame-type-header:first-child + .frame { padding-top: 0 }` has specificity (0,4,0) and beats the utility class `.t3b-pt-l` (0,1,0). The editor then picks a spacing step and **nothing happens** — no warning, the field simply appears broken. Confirm by simulating an editor: swap the class in the browser to the strongest step and read the computed value; if it stays put, a CSS rule is winning. → Express spacing in `spacing.yaml` (defaults) or `tx_t3b_spacing` (exceptions), never in the SCSS partial. - **Positional rules leak into containers.** `first`/`last` is computed per `(pid, colPos, language, tx_container_parent)`, so a `ctypes..first` rule ALSO matches elements that are first inside a *container column*. Measure the blast radius before writing the rule: `SELECT parent.CType, COUNT(*) FROM tt_content c JOIN tt_content parent ON parent.uid=c.tx_container_parent WHERE c.CType='' AND c.deleted=0 AND c.tx_container_parent>0 GROUP BY parent.CType;` Neutralise with `containers..first:` re-asserting the extension defaults (harmless for other CTypes, since it only restates what they already inherit). - **The system has no notion of "has a background".** The same frame padding is dead whitespace on a plain element but becomes the *inner padding of a band* once the element carries a `bg-*` class — so one positional default cannot serve both, and requirements like "less gap under the page title" and "more gap inside the grey box" only *look* contradictory. Keep the default, and set the exception per element (`{"padding_top":"none"}`) — it stays visible and editable in the backend. Find the elements needing it (first element after a page-title header, no band class): `… WHERE c1.CType='header' AND c1.colPos=0 AND c2.element_classes NOT LIKE '%bg-%' …` — do this before estimating effort, the count is usually far smaller than it looks. - **When SCSS IS still correct:** the spacing system works per content ELEMENT, never per column. A two-column element where one column must stay flush (a background-media image) while the other needs an inset can only be solved in CSS — target the inner column (e.g. `.ce-bodytext`), not the frame, so you are not overriding anything the editor could have set. - **Verify from the emitted markup, not the rendering:** `curl -s "?cb=$RANDOM" | grep -o 't3b-pt-[a-z]*'`. `spacing.yaml` is read at render time and the result is page-cached → `cache:flush` after editing it, and mind the browser's cached `main.css` (see the cache section). **General principle this is an instance of:** when a theme offers an editor-facing control for something, configure *that* control; a CSS override of it is invisible to the editor, outranks their input, and turns a self-service setting into a support ticket. --- ## 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+ WHERE 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. =< 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: `.{box-shadow:0 0 0 100vw ;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:;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** (``), 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 `