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) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
# OS / editor junk
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
@@ -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
|
||||||
|
- `<SSH_USER>` `<SSH_HOST>` `<SSH_PORT>` — from konsoleH → *Einstellungen → Logindaten* (SSH-Login).
|
||||||
|
The user's SSH public key should already be registered there (Öffentliche SSH-Schlüssel).
|
||||||
|
- `<WEBROOT>` — the account web root, typically `/usr/www/users/<SSH_USER>` (== `~/public_html`, a
|
||||||
|
root-owned symlink you **cannot** repoint).
|
||||||
|
- `<PROJECT>` — a subdir under the webroot for the project root, e.g. `typo3-14` or `app`
|
||||||
|
(so the app lives at `<WEBROOT>/<PROJECT>` and the docroot becomes `<PROJECT>/public`).
|
||||||
|
- `<STAGING_DOMAIN>` — the staging hostname (e.g. a `*.kunden.<agency>.systems` subdomain you control).
|
||||||
|
- `<DB_HOST> <DB_NAME> <DB_USER>` — 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 <SSH_PORT> -o BatchMode=yes <SSH_USER>@<SSH_HOST>"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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/<PROJECT> 2>/dev/null;
|
||||||
|
php -r "echo ini_get(\"memory_limit\");";
|
||||||
|
# outbound reachability for composer git + DB host:
|
||||||
|
timeout 6 bash -c "echo > /dev/tcp/<git-host>/22" && echo git-ok;
|
||||||
|
timeout 6 bash -c "echo > /dev/tcp/<DB_HOST>/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 "<SSH_USER>@<SSH_HOST>-deploy"; ssh-keyscan -T 8 <git-host> >> ~/.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-host>:<owner>/<repo>.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 `<DB_NAME>/<DB_USER>/<DB_HOST>`; password stays with the user.
|
||||||
|
2. **Docroot**: in the **domain settings** (domain admin / *Verwaltung* → the domain's
|
||||||
|
*Dokumentstamm / Zielverzeichnis*), set the document root to **`<PROJECT>/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 `<PROJECT>/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 <SSH_PORT>" composer.json composer.lock config <SSH_USER>@<SSH_HOST>:<WEBROOT>/<PROJECT>/
|
||||||
|
```
|
||||||
|
(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/<PROJECT> && 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/<private-pkgs>` 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/<PROJECT> && git -C vendor/<vendor>/<pkg> rev-parse --short HEAD' # compare to the pushed branch HEAD
|
||||||
|
$SSH 'cd ~/public_html/<PROJECT> && php -d memory_limit=-1 ~/bin/composer update <vendor>/<pkg> --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/<PROJECT> && 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 `<WEBROOT>/<PROJECT>/config/system/additional.php`
|
||||||
|
(**contains no secret** — the DB password is read at runtime from a secrets file):
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
(static function (): void {
|
||||||
|
$secret = __DIR__ . '/.env.db';
|
||||||
|
$dbPassword = is_file($secret) ? trim((string) file_get_contents($secret)) : '';
|
||||||
|
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default'] = array_replace(
|
||||||
|
$GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default'] ?? [],
|
||||||
|
['driver' => 'mysqli', 'host' => '<DB_HOST>', 'port' => 3306,
|
||||||
|
'dbname' => '<DB_NAME>', 'user' => '<DB_USER>', 'password' => $dbPassword, 'charset' => 'utf8mb4']
|
||||||
|
);
|
||||||
|
$GLOBALS['TYPO3_CONF_VARS']['SYS']['trustedHostsPattern'] = '<STAGING_DOMAIN regex, dots escaped>';
|
||||||
|
$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/<PROJECT>/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/<PROJECT> && sed -i "s#^base: .*#base: /#" config/sites/<site>/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 <SSH_PORT> /tmp/db.sql.gz <SSH_USER>@<SSH_HOST>:~/db.sql.gz
|
||||||
|
$SSH 'cd ~/public_html/<PROJECT> && zcat ~/db.sql.gz | MYSQL_PWD="$(cat config/system/.env.db)" mysql -h <DB_HOST> -u <DB_USER> <DB_NAME>'
|
||||||
|
$SSH 'MYSQL_PWD="$(cat ~/public_html/<PROJECT>/config/system/.env.db)" mysql -h <DB_HOST> -u <DB_USER> -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=\"<DB_NAME>\";"'
|
||||||
|
```
|
||||||
|
(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 <SSH_PORT>" public/fileadmin/ <SSH_USER>@<SSH_HOST>:<WEBROOT>/<PROJECT>/public/fileadmin/
|
||||||
|
```
|
||||||
|
(`_processed_` is regenerated on demand by ImageMagick — no need to transfer.)
|
||||||
|
|
||||||
|
## Step 11 — Finalize TYPO3
|
||||||
|
```
|
||||||
|
$SSH 'cd ~/public_html/<PROJECT> && 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/<PROJECT>/.htpasswd <auth_user>'
|
||||||
|
```
|
||||||
|
Prepend the auth block to `public/.htaccess` (no secret):
|
||||||
|
```
|
||||||
|
$SSH 'cd ~/public_html/<PROJECT> && { printf "AuthType Basic\nAuthName \"Staging\"\nAuthUserFile <WEBROOT>/<PROJECT>/.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://<STAGING_DOMAIN>/` → 500 = htpasswd unreadable, 401 = healthy. Fix with
|
||||||
|
`$SSH 'chmod 644 ~/public_html/<PROJECT>/.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://<STAGING_DOMAIN>/ # 401 (auth on)
|
||||||
|
curl -sS -u <auth_user>:<pass> -L -o /dev/null -w "%{http_code}\n" http://<STAGING_DOMAIN>/ # 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/<site>/config.yaml` (base `/`), `public/.htaccess` (basic-auth), `public/fileadmin`
|
||||||
|
(sync separately). To pull pushed theme/extension changes, re-run the relevant
|
||||||
|
`composer update <vendor>/<pkg>` 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).
|
||||||
@@ -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/<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.
|
||||||
|
- **Plain-text bodytext fields (hero-item etc.):** some CTypes store `bodytext` as PLAIN text and the
|
||||||
|
template wraps it in `<p>` + escapes on render. Storing your own `<p>…</p>` (or html-escaping)
|
||||||
|
double-escapes → literal `<p>` shows on the page. Store the raw sentence only.
|
||||||
|
- **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).
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
---
|
||||||
|
name: typo3-v11-to-v14-ddev-upgrade
|
||||||
|
description: "Bring an existing TYPO3 v11 site up and booting on TYPO3 v14 LTS (v14.3) locally under DDEV, starting from a v11 SQL dump + the old sitepackage. Use when upgrading/migrating a TYPO3 v11 (or v12/13) project to v14 for local dev, when a v11 database dump must be made to boot on v14, or when a t3bootstrap-based v11 site needs its composer/config/schema/upgrade-wizards/site-sets brought to v14. Covers: composer.json rework to ^14 + private git repos & composer registry auth, config/system/settings.php, the v14 .htaccess, database:updateschema, backend admin, upgrade wizards (incl. confirmable + one-way ones), language packs, wiring t3bootstrap TypoScript via site sets, and the recurring v11->v14 traps. NOT for fresh installs and NOT for server deployment (see the konsoleH staging-deploy skill for that)."
|
||||||
|
metadata:
|
||||||
|
author: Wappler
|
||||||
|
version: "1.0"
|
||||||
|
---
|
||||||
|
|
||||||
|
# TYPO3 v11 → v14 upgrade (local, DDEV)
|
||||||
|
|
||||||
|
Get an existing **TYPO3 v11 site** installable and **booting on v14 LTS (14.3.x)** under **DDEV**,
|
||||||
|
from a **v11 SQL dump** + the old sitepackage. This is the "make it run" phase — a later phase
|
||||||
|
matches the frontend design (see the *t3bootstrap live-design parity* skill) and another deploys to
|
||||||
|
staging (see the *konsoleH staging-deploy* skill).
|
||||||
|
|
||||||
|
Work **one step at a time and verify each** before moving on. **`ddev snapshot` before anything
|
||||||
|
destructive** (especially upgrade wizards — several are one-way). Never let a live secret appear in a
|
||||||
|
command. Many errors here look like config bugs but are data/schema/host issues — always confirm the
|
||||||
|
*actual* error before "fixing".
|
||||||
|
|
||||||
|
## Placeholders to resolve first
|
||||||
|
- `<PROJECT_DIR>` — the DDEV project root (docroot `public/`).
|
||||||
|
- `<SITE_ID>` — the site identifier folder under `config/sites/<SITE_ID>/`.
|
||||||
|
- `<SITE_BASE_HOST>` — the site's base host from `config/sites/<SITE_ID>/config.yaml` (e.g. the
|
||||||
|
`*.ddev.site` host). The FE only answers on this host, NOT `localhost`.
|
||||||
|
- `<REGISTRY_URL>` — the private Composer registry for the theme/vendor packages, if any (e.g. a
|
||||||
|
Gitea `.../api/packages/<vendor>/composer` URL). `<GIT_REPOS>` — any `gitea@`/GitHub SSH git repos
|
||||||
|
in `composer.json` `repositories`.
|
||||||
|
- `<TEMPLATE_PACKAGE>` — the sitepackage's composer `name` (⚠ often ≠ repo name — read the repo's
|
||||||
|
own `composer.json`). `<TEMPLATE_EXTKEY>` — its extension key (keep it stable across the rename).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 0 — DDEV up + import the dump (read-only baseline)
|
||||||
|
`ddev start`; import the v11 dump into the `db` database (`ddev import-db --file=dump-*.sql` or
|
||||||
|
`ddev mysql < dump.sql`). Confirm PHP/DB versions match the target (v14.3 wants PHP 8.2–8.5). Snapshot:
|
||||||
|
`ddev snapshot --name pre-upgrade`.
|
||||||
|
|
||||||
|
## Step 1 — Rework `composer.json` for v14
|
||||||
|
- Bump every `typo3/cms-*` to `^14`. Bump third-party exts (news, address, t3bootstrap/*, …) to their
|
||||||
|
^14 releases. Drop anything with no v14 release (e.g. **flux/vhs are gone in v14 t3bootstrap** —
|
||||||
|
`t3bootstrap/flux-*` do not exist; do not re-add them).
|
||||||
|
- **Custom-repo package name ≠ repo name.** A git repo `…/<name>.git` may publish itself as a
|
||||||
|
*different* composer package name — read the repo's `composer.json` `name` and require THAT.
|
||||||
|
- **Contradictory constraints fail:** `"^14 dev-release/v14"` (tag AND branch) → composer "cannot
|
||||||
|
possibly match". Use plain `"^14"` for a tagged package, or `"dev-<branch>"` for an untagged one.
|
||||||
|
- **Repository order controls the source.** If the same package name is served by both a git repo and
|
||||||
|
the registry, composer takes the **first** repo listed as canonical → list the project's git repo
|
||||||
|
**before** the composer registry in `repositories` to make it win.
|
||||||
|
|
||||||
|
## Step 2 — Composer auth
|
||||||
|
- SSH git repos: `ddev auth ssh` (loads your keys into the DDEV agent; covers `gitea@…`/GitHub).
|
||||||
|
- Private registry: `ddev composer config http-basic.<registry-host> <user> <token>` (writes the
|
||||||
|
project `auth.json`). Get the token from the user — never invent/guess it.
|
||||||
|
|
||||||
|
## Step 3 — Install
|
||||||
|
`ddev composer update -W` (resolve + install with dependencies). Fix constraint conflicts per Step 1.
|
||||||
|
|
||||||
|
## Step 4 — Create `config/system/settings.php`
|
||||||
|
v14 does **not** auto-write `settings.php`; `ConfigurationManager` reads it if present, else falls
|
||||||
|
back to the old `typo3conf/LocalConfiguration.php` in place — so a v11→v14 jump with **production DB
|
||||||
|
creds** in the old file fails to boot first. Create `settings.php` from the old
|
||||||
|
`LocalConfiguration.php`, but with **DDEV DB creds** (host `db`, name `db`, user `db`, pass `db`, port
|
||||||
|
3306, `utf8mb4`). Preserve `SYS/encryptionKey`, install-tool password, sitename, systemMaintainers,
|
||||||
|
mail sender, and per-extension `EXTENSIONS` config. Drop obsolete v11 feature flags.
|
||||||
|
|
||||||
|
## Step 5 — Replace `public/.htaccess`
|
||||||
|
Swap in the v14 template:
|
||||||
|
`cp vendor/typo3/cms-install/Resources/Private/FolderStructureTemplateFiles/root-htaccess public/.htaccess`.
|
||||||
|
Symptom of the stale v11 `.htaccess`: Apache **500 "Request exceeded the limit of 10 internal
|
||||||
|
redirects"** (looks like a config error; it's the htaccess).
|
||||||
|
|
||||||
|
## Step 6 — Update the DB schema
|
||||||
|
`ddev exec vendor/bin/typo3 database:updateschema "*.add,*.change"` — adds the ~20 tables v12–v14 need
|
||||||
|
(e.g. `sys_csp_resolution`). Non-destructive (add/change only). Symptom if skipped:
|
||||||
|
`Table 'db.sys_csp_resolution' doesn't exist` on any backend request.
|
||||||
|
|
||||||
|
## Step 7 — Backend admin
|
||||||
|
`ddev exec vendor/bin/typo3 backend:createadmin <user> <pass>` (or `backend:resetpassword` if an admin
|
||||||
|
already exists in the dump). Do NOT create admin users the user didn't ask for on shared/live systems;
|
||||||
|
local DDEV is fine.
|
||||||
|
|
||||||
|
## Step 8 — Upgrade wizards (⚠ snapshot first)
|
||||||
|
`ddev snapshot --name pre-upgradewizards`. Then `vendor/bin/typo3 upgrade:list` and run wizards:
|
||||||
|
- **`upgrade:run` gates on a "Database Up-to-Date" prerequisite** → clear data blockers, re-run
|
||||||
|
`database:updateschema`, then `upgrade:run`. Blockers commonly seen:
|
||||||
|
- `tx_news_domain_model_news.related_links` NULL/non-int → `UPDATE … SET related_links=0 WHERE
|
||||||
|
related_links IS NULL OR NOT related_links REGEXP '^[0-9]+$'`.
|
||||||
|
- `sys_category_record_mm` duplicate PK tuples (table has no `uid`) → add a temp AUTO_INCREMENT PK,
|
||||||
|
`DELETE` via self-join keeping the lowest, drop the temp column.
|
||||||
|
- **Confirmable wizards** (e.g. `convertHeadersize`, `rewriteIconPaths`) error *"You have to
|
||||||
|
acknowledge this wizard"* under `--no-interaction`. There is **no `--confirm` flag** (`--confirm all`
|
||||||
|
throws). Run interactively with piped input: `printf 'yes\n' | ddev exec vendor/bin/typo3
|
||||||
|
upgrade:run <id>` (omit `--no-interaction`).
|
||||||
|
- **One-way wizards** (e.g. container migration, address→contacts) — the snapshot is your only undo.
|
||||||
|
- **news 14.0.x vs `FlexFormTools::cleanFlexFormXML()`**: older news calls it with 3 args, v14.3 wants
|
||||||
|
4 → blocks `txNewsPluginUpdater`. Fix: update news to a patched 14.x, or one-time-patch the vendor
|
||||||
|
`PluginUpdater` (the wizard marks itself done; the patch is then harmless if reverted).
|
||||||
|
|
||||||
|
## Step 8b — Language packs
|
||||||
|
`ddev exec vendor/bin/typo3 language:update`. Composer mode ships **no** language packs → labels fall
|
||||||
|
back to English (e.g. news "more-link"). Run this whenever the site language isn't English.
|
||||||
|
|
||||||
|
## Step 9 — Wire TypoScript via site sets
|
||||||
|
v14 t3bootstrap delivers TypoScript (incl. the `PAGE` object) through **site sets**, not `sys_template`.
|
||||||
|
The site config must declare them under `dependencies:` in `config/sites/<SITE_ID>/config.yaml`, or the
|
||||||
|
FE dies with **"No page configured for type=0"**. Add the set identifiers (the `name:` in each ext's
|
||||||
|
`Configuration/Sets/*/config.yaml` — independent of package name/extkey), e.g. the theme set +
|
||||||
|
container-bs5-templates + news/form/address/blog/slider as used. See the design skill for details.
|
||||||
|
|
||||||
|
## Step 10 — Flush + verify
|
||||||
|
`ddev exec vendor/bin/typo3 cache:flush`. Verify over HTTP with the **correct host**: `/typo3/` → 200,
|
||||||
|
FE with `curl -H "Host: <SITE_BASE_HOST>" …` (or the real DDEV URL) → 200. **"No site configuration
|
||||||
|
found" is usually a false alarm from curling `localhost`** instead of `<SITE_BASE_HOST>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Other recurring traps
|
||||||
|
- **Leftover custom content element with no v14 provider → FE fatal** empty-`templateName`
|
||||||
|
`InvalidTemplateResourceException`. Don't assume it's flux — check the old sitepackage; a plain
|
||||||
|
custom FSC CType must be re-registered for v14 (see the design skill's iconmenu example).
|
||||||
|
- **Relocating a git-repo package's vendor path:** change the `name` in *that repo's own*
|
||||||
|
`composer.json` (composer installs by declared name, not repo URL). Keep the `extension-key`
|
||||||
|
unchanged so `EXT:<key>/…` refs + set names keep working. Untagged repo → require `dev-<branch>`.
|
||||||
|
- **v14 FlexForm relational fields:** `FlexFormFieldValues::get()` returns a `LazyRecordCollection` /
|
||||||
|
`RecordInterface` for relation fields (NOT a CSV of uids), which breaks `intExplode()`/`(int)` casts
|
||||||
|
in migrated/old ext code → `TypeError`. Normalize to `(string)`/`(string)$record->getUid()` before
|
||||||
|
parsing. Reproduce/verify from CLI with a small bootstrap script (SystemEnvironmentBuilder::run +
|
||||||
|
Bootstrap::init + the record API).
|
||||||
|
- **TYPO3 Console** (`helhum/typo3-console` ^8.3) works with v14 — `vendor/bin/typo3` is the console
|
||||||
|
binary.
|
||||||
|
|
||||||
|
Each step verified + a snapshot before wizards = a reversible, debuggable upgrade.
|
||||||
Reference in New Issue
Block a user