commit 4e4af79e4fa5f2c4756eeee685a4a3a00da83254 Author: Ilja Melnicenko Date: Mon Jul 20 12:01:06 2026 +0200 Initial commit: TYPO3 v11→v14 upgrade / design-parity / konsoleH-deploy skills Three reusable, placeholder-based Claude Code skills distilled from the IVV Aachen TYPO3 v11→v14 project: - typo3-v11-to-v14-ddev-upgrade - typo3-t3bootstrap-live-design-parity (v1.1) - typo3-konsoleh-staging-deploy Co-Authored-By: Claude Opus 4.8 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83484c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# OS / editor junk +.DS_Store +Thumbs.db +*.swp +*~ +.idea/ +.vscode/ diff --git a/typo3-konsoleh-staging-deploy/SKILL.md b/typo3-konsoleh-staging-deploy/SKILL.md new file mode 100644 index 0000000..87b8d76 --- /dev/null +++ b/typo3-konsoleh-staging-deploy/SKILL.md @@ -0,0 +1,226 @@ +--- +name: typo3-konsoleh-staging-deploy +description: "Deployt ein lokal mit DDEV gebautes TYPO3-Projekt (v13/v14, Composer) als Staging-Seite auf einen Hetzner-Managed-Webhosting-Server (konsoleH). Der Composer-Build läuft AUF dem Server (kein vorgefertigtes vendor/ wird hochgeladen). Verwenden, wenn eine TYPO3-Staging- oder Vorschau-Umgebung auf Hetzner Managed Hosting (konsoleH), einem *.your-server.de-Host oder dedi*-Server eingerichtet werden soll, oder wenn eine lokale DDEV-TYPO3-Seite per SSH auf ein Managed-/Shared-Hosting übertragen werden soll. Umfasst: SSH-Inventar, per-User-composer.phar, Deploy-Keys für private Git-Repos (gitea) + Registry-Auth, Datenbank + Dokumentstamm über konsoleH, rsync von Quellcode + fileadmin, DB-Import, Staging-additional.php, .htaccess, Sprachpakete, HTTP-Basic-Auth und Let's Encrypt. NICHT für Root-VPS-/Docker-/DDEV-auf-dem-Server-Deployments." +metadata: + author: Wappler + version: "1.0" +--- + +# TYPO3 → Hetzner konsoleH staging deployment + +Deploy a **DDEV-built TYPO3 composer project** to **Hetzner Managed Webhosting (konsoleH)** as a +staging site. This is **managed hosting**: **no root, no Docker, no systemd, no package installs.** +DB, PHP version, cronjobs, domains, docroot and SSL are configured through konsoleH panels; code and +data go over **SSH/SFTP (usually a non-standard port like 222)**. The build is **composer-based on the +server** (run `composer install` on the host — do NOT rsync a pre-built `vendor/`). + +Work **step by step**, verify each step, and never let a live secret appear in a command (see +*Secret handling*). Substitute the placeholders below from the target project. + +## Placeholders to resolve first +- `` `` `` — from konsoleH → *Einstellungen → Logindaten* (SSH-Login). + The user's SSH public key should already be registered there (Öffentliche SSH-Schlüssel). +- `` — the account web root, typically `/usr/www/users/` (== `~/public_html`, a + root-owned symlink you **cannot** repoint). +- `` — a subdir under the webroot for the project root, e.g. `typo3-14` or `app` + (so the app lives at `/` and the docroot becomes `/public`). +- `` — the staging hostname (e.g. a `*.kunden..systems` subdomain you control). +- ` ` — from konsoleH → *Einstellungen → MariaDB/MySQL* (the DB is on a + **separate host** like `*.your-database.de`, NOT localhost). Password is entered by the user, never + by you. +- Private composer repos — read `repositories` in the project's `composer.json` (e.g. `gitea@...` + SSH git repos + an authenticated composer registry URL). + +Shorthand used below: `SSH="ssh -p -o BatchMode=yes @"`. + +--- + +## Step 0 — SSH inventory (read-only) +Confirm access and capabilities before changing anything: +``` +$SSH 'id -un; echo $HOME; ls -la ~; readlink ~/public_html; + command -v php git mysql mysqldump node; php -v | head -1; cat ~/.phpversion; + ls -d ~/public_html/ 2>/dev/null; + php -r "echo ini_get(\"memory_limit\");"; + # outbound reachability for composer git + DB host: + timeout 6 bash -c "echo > /dev/tcp//22" && echo git-ok; + timeout 6 bash -c "echo > /dev/tcp//3306" && echo db-ok' +``` +Expect: PHP matching the project's `config.platform.php`, `composer/git/mysql/node` present, +ImageMagick usually at `/usr/bin/` (`convert`), `memory_limit` often only 128M (→ always run +composer/typo3 with `php -d memory_limit=-1`). + +## Step 1 — Install a current Composer per-user +The system `/usr/bin/composer` is usually **old** and spews PHP-version deprecation noise. Install the +latest phar for this account: +``` +$SSH 'mkdir -p ~/bin && curl -sSL https://getcomposer.org/download/latest-stable/composer.phar -o ~/bin/composer && chmod +x ~/bin/composer && php ~/bin/composer --version' +``` +Use `~/bin/composer` for every composer command afterwards. + +## Step 2 — Private-repo auth for composer +Inspect `composer.lock` to see what each package resolves from: +``` +# locally: +php -r '$l=json_decode(file_get_contents("composer.lock"),true); foreach($l["packages"] as $p){ $s=$p["source"]["url"]??""; $d=$p["dist"]["url"]??""; if(str_contains($s,"@")||str_contains($d,"your-registry")) echo $p["name"]." src=$s dist=$d\n"; }' +``` +- **SSH git repos** (`source.url = gitea@host:...`) → generate a **deploy key on the server** and have + the user add its public key to **each** such repo (deploy keys are per-repo): + ``` + $SSH 'test -f ~/.ssh/id_ed25519 || ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" -C "@-deploy"; ssh-keyscan -T 8 >> ~/.ssh/known_hosts 2>/dev/null; cat ~/.ssh/id_ed25519.pub' + ``` + Give the user the pubkey; after they add it, verify: + `$SSH 'GIT_SSH_COMMAND="ssh -o BatchMode=yes" git ls-remote gitea@:/.git | head -1'` +- **Authenticated composer registry** (dist zips over HTTPS, returns 401 anonymously) → the user + supplies the registry **username + token** interactively at `composer install` (composer saves it to + `auth.json`). It is usually **not recoverable locally** (DDEV composer also 401s; local installs work + only from the lock + existing vendor). + +## Step 3 — konsoleH panel steps (user does these) +1. **Database**: *MariaDB/MySQL* → note `//`; password stays with the user. +2. **Docroot**: in the **domain settings** (domain admin / *Verwaltung* → the domain's + *Dokumentstamm / Zielverzeichnis*), set the document root to **`/public`**. + The *Serverkonfiguration (.htaccess)* page is NOT the docroot — it is only per-folder `.htaccess` + + *Verzeichnisschutz* (password). If no docroot field exists, fall back to serving `/public` + via symlinks from the webroot over SSH (last resort — prefer the panel setting). +3. **SSL** (do at the end): *SSL-Manager* → set the domain's certificate to **Let's Encrypt** + (otherwise HTTPS is invalid; `https://` only works with `curl -k`). + +## Step 4 — Ship the source (NOT vendor) +``` +rsync -az -e "ssh -p " composer.json composer.lock config @:// +``` +(Ships `composer.json`, `composer.lock`, `config/`. The DDEV `config/system/settings.php` + +`additional.php` are fine to ship — the DDEV block is guarded by `IS_DDEV_PROJECT` and won't run on the +server; you'll override DB via a staging `additional.php` in Step 8.) + +## Step 5 — composer install (user enters registry auth) +Have the **user** run it in their own SSH session so the registry token prompt stays with them: +``` +cd ~/public_html/ && php -d memory_limit=-1 ~/bin/composer install --no-dev -o +``` +Answer `y` to save credentials. Verify: `vendor/bin/typo3 --version` shows the right TYPO3 version + +context; `vendor/` present; `public/index.php` scaffolded. + +## Step 6 — ⚠️ Make the locked git references current (BIGGEST TRAP) +`composer.lock` pins each **git** package to a specific commit `reference`. If template/theme changes +were pushed **after** the lock was last generated, `composer install` checks out the **stale** commit — +silently shipping an OLD theme (missing logos/assets/SCSS) → 500s like *"Unable to render image URI"*. +Verify and, if stale, update on the server (it has the deploy key + saved registry auth): +``` +$SSH 'cd ~/public_html/ && git -C vendor// rev-parse --short HEAD' # compare to the pushed branch HEAD +$SSH 'cd ~/public_html/ && php -d memory_limit=-1 ~/bin/composer update / --no-interaction' +``` + +## Step 7 — Add `public/.htaccess` (composer does NOT scaffold it) +Without it, only `/` works and every routed path (e.g. `/home`) → Apache 404: +``` +$SSH 'cd ~/public_html/ && cp vendor/typo3/cms-install/Resources/Private/FolderStructureTemplateFiles/root-htaccess public/.htaccess' +``` + +## Step 8 — Staging config via `config/system/additional.php` +Write this file locally and `scp` it to `//config/system/additional.php` +(**contains no secret** — the DB password is read at runtime from a secrets file): +```php + 'mysqli', 'host' => '', 'port' => 3306, + 'dbname' => '', 'user' => '', 'password' => $dbPassword, 'charset' => 'utf8mb4'] + ); + $GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] = ''; + $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor'] = 'ImageMagick'; // if convert is at /usr/bin + $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path'] = '/usr/bin/'; + $GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path_lzw'] = '/usr/bin/'; + $GLOBALS['TYPO3_CONF_VARS']['SYS']['displayErrors'] = 1; // helpful behind basic-auth; turn off later +})(); +``` +The user creates the secrets file (password never touches your commands — see *Secret handling*): +``` +umask 077; read -rs -p "Staging DB password: " P; printf '%s' "$P" > ~/public_html//config/system/.env.db; unset P; echo +``` +Set the **site base** to host-relative so one config works on any host + before SSL: +``` +$SSH 'cd ~/public_html/ && sed -i "s#^base: .*#base: /#" config/sites//config.yaml' +``` + +## Step 9 — Import the database +Dump locally, ship, import (password read from the secrets file, never inline): +``` +ddev export-db --file=/tmp/db.sql.gz +scp -P /tmp/db.sql.gz @:~/db.sql.gz +$SSH 'cd ~/public_html/ && zcat ~/db.sql.gz | MYSQL_PWD="$(cat config/system/.env.db)" mysql -h -u ' +$SSH 'MYSQL_PWD="$(cat ~/public_html//config/system/.env.db)" mysql -h -u -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=\"\";"' +``` +(A `ddev export-db` dump is clean — no `USE`/`CREATE DATABASE`/`DEFINER`. If importing an arbitrary +dump, check for those first.) + +## Step 10 — Ship uploaded files (fileadmin) +``` +rsync -az --exclude='_processed_/' --exclude='_temp_/' -e "ssh -p " public/fileadmin/ @://public/fileadmin/ +``` +(`_processed_` is regenerated on demand by ImageMagick — no need to transfer.) + +## Step 11 — Finalize TYPO3 +``` +$SSH 'cd ~/public_html/ && php -d memory_limit=-1 vendor/bin/typo3 database:updateschema "*.add,*.change"; php -d memory_limit=-1 vendor/bin/typo3 language:update; php -d memory_limit=-1 vendor/bin/typo3 cache:flush' +``` +`language:update` is required or labels fall back to English (e.g. news "Read more" vs "Weiterlesen"). + +## Step 12 — HTTP basic-auth +Put `.htpasswd` **outside** the public docroot; user creates it interactively: +``` +$SSH ... (user runs) 'htpasswd -Bc ~/public_html//.htpasswd ' +``` +Prepend the auth block to `public/.htaccess` (no secret): +``` +$SSH 'cd ~/public_html/ && { printf "AuthType Basic\nAuthName \"Staging\"\nAuthUserFile //.htpasswd\nRequire valid-user\n\n"; cat public/.htaccess; } > public/.htaccess.new && mv public/.htaccess.new public/.htaccess' +``` +**GOTCHA — `chmod 644 .htpasswd`:** Apache's auth worker runs as a **different uid than PHP**, so a +`600` file gives **500 on credential submission** (no-creds still 401). Diagnose: +`curl -u bad:bad http:///` → 500 = htpasswd unreadable, 401 = healthy. Fix with +`$SSH 'chmod 644 ~/public_html//.htpasswd'`. It's outside the web root and only a bcrypt hash. + +## Step 13 — Verify +``` +curl -sS -o /dev/null -w "%{http_code}\n" http:/// # 401 (auth on) +curl -sS -u : -L -o /dev/null -w "%{http_code}\n" http:/// # 200 +``` +Read the FE HTML for the expected title + content markers; check the TYPO3 log +(`var/log/typo3_*.log`) for exceptions. Confirm the backend `/typo3/` login works. When the user +completes the SSL-Manager → Let's Encrypt step, re-check `https://` without `-k`. + +--- + +## Secret handling (mandatory) +The environment blocks any command containing a **literal live credential**. So: +- The user introduces the DB password **once** into `config/system/.env.db` (`chmod 600`, `read -rs`), + and the TYPO3 password/registry token **interactively** in their own SSH session. +- Your admin commands read the DB password from the file: `MYSQL_PWD="$(cat …/.env.db)" mysql …` — + the literal secret never appears in the command text. +- Never write a password into a command, a config file you author, or the transcript. `additional.php` + reads the password from `.env.db`; it never embeds it. + +## Redeploy / update (env-specific files — do NOT overwrite) +On subsequent rsyncs, **exclude** the files that hold staging-specific state: +`config/system/additional.php`, `config/system/.env.db`, `config/system/settings.php`, +`config/sites//config.yaml` (base `/`), `public/.htaccess` (basic-auth), `public/fileadmin` +(sync separately). To pull pushed theme/extension changes, re-run the relevant +`composer update /` on the server (Step 6), then `cache:flush`. + +## Consolidated gotchas +1. Managed hosting = no root/Docker/DDEV — everything via SSH + konsoleH panels. +2. System composer is stale → install the latest phar per-user (`~/bin/composer`). +3. `composer.lock` pins **git** package commit refs → stale template unless you `composer update` it. +4. `public/.htaccess` is NOT scaffolded by composer → copy the cms-install template. +5. Docroot is a **domain** setting, not the `.htaccess`/Serverkonfiguration page. +6. DB is on a **separate host**; created via panel; password only via `.env.db`. +7. Site base `/` (host-relative) avoids per-env/pre-SSL URL issues. +8. `language:update` on the server, or labels fall back to English. +9. `.htpasswd` must be `chmod 644` (Apache auth uid ≠ PHP uid) and live outside the web root. +10. SSL = admin sets Let's Encrypt in the konsoleH SSL-Manager. +11. Always run composer/typo3 with `php -d memory_limit=-1` (128M host limit). diff --git a/typo3-t3bootstrap-live-design-parity/SKILL.md b/typo3-t3bootstrap-live-design-parity/SKILL.md new file mode 100644 index 0000000..3797ee0 --- /dev/null +++ b/typo3-t3bootstrap-live-design-parity/SKILL.md @@ -0,0 +1,208 @@ +--- +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.1" +--- + +# 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/`. + +--- + +## 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 `