53be3ef898
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>
220 lines
19 KiB
Markdown
220 lines
19 KiB
Markdown
---
|
|
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, 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.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 `@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/`.
|
|
|
|
---
|
|
|
|
## 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/<X> not found" (news) / "List/<X> 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).
|