Files
t3bootstrap-upgrade-skills/typo3-t3bootstrap-live-design-parity/SKILL.md
T
melnicenkoandClaude Opus 5 674fbc9e46 design-parity skill v1.3: t3b_core spacing system
Add a dedicated section on where spacing is resolved (Spacing.yaml ->
per-site spacing.yaml -> tx_t3b_spacing) and why a spacing fix in the
custom SCSS partial silently disables the editor-facing field: the
utility class it emits is (0,1,0) and loses to any contextual rule.

Also covers the first/last-position leak into container columns, the
fact that the system has no notion of "has a background" (so seemingly
contradictory spacing complaints are two different cases), when SCSS is
still the right tool (per-column, not per-element), and how to verify
from the emitted markup instead of the rendering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 13:30:53 +02:00

23 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, 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).
author version
Wappler 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/<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/.

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/<SITE_ID>/spacing.yaml — per site, itself a cascade: defaults:first: / last: (position in the column) → ctypes.<CType>:containers.<containerCType>: (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}-<step> 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.<X>.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='<X>' AND c.deleted=0 AND c.tx_container_parent>0 GROUP BY parent.CType; Neutralise with containers.<containerCType>.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 "<url>?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+<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).