Files
tsv08kulmbach-website/docs/deploy.md
fs 06a18a2d19 301-Weiterleitungen der alten CMS-Adressen
Der Aufnahmeantrag der Turnabteilung ging als alte URL in den Einladungen an
die neuen Familien raus und lief seit dem Umzug in einen 404, ebenso der Link
auf die Turn-Seite. Beide leiten jetzt weiter, dazu alle uebrigen Seiten der
alten Navigation unter /08/de/.

Die PDF-Regel matcht am Umlaut vorbei: der Dateiname kommt je nach Mailprogramm
als NFC, als NFD (so stand er in der alten Datenbank) oder roh als UTF-8 an.
Die Sammelregel wirft den alten Query-String weg, sonst haengt mod_rewrite
?group_filter_id=14 ans neue Ziel.

Erledigt damit den offenen Punkt F3 aus docs/deploy.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWoqDYuANFbJDGFQAgaRdT
2026-07-31 22:11:08 +02:00

389 lines
16 KiB
Markdown

# Deploy auf All-Inkl (Shared Hosting, SSH + Cronjobs)
Schritt für Schritt vom leeren Webspace zur Live-Seite. Die Reihenfolge ist Absicht:
**die alte CMS-Seite bleibt online, bis die neue auf einer geschützten Test-Subdomain
nachweislich läuft.** Umgeschaltet wird erst am Ende, und das Umschalten ist in einer
Minute rückgängig.
Der Kern des Plans: die Test-Subdomain läuft mit **derselben** `config.php`, die
später live geht. Es gibt keinen Config-Wechsel beim Umschalten, also auch keinen
Fehler, der sich dabei einschleichen kann. Möglich ist das, weil die
Host-Weiterleitung in `public/.htaccess` nur beim exakten Host `tsv08kulmbach.de`
greift; eine Subdomain läuft unbehelligt durch. Einzige Abweichung: Canonical, OG
und Sitemap nennen auf der Test-Subdomain schon die echte Domain. Genau richtig für
den Umschaltmoment, und dank Verzeichnisschutz sieht es ohnehin keine Suchmaschine.
Legende: `[lokal]` auf deinem Mac, `[SSH]` in der Server-Shell, `[KAS]` im
All-Inkl-Kundenmenü.
---
## Teil A — Vorbereitung lokal
### A1 [lokal] Die zwei offenen Werte in `config/config.production.php` setzen
`php bin/preflight.php` bricht sonst auf dem Server ab. Beides betrifft nur die
Dateiablage `/dateien`, nicht die Website selbst.
**Passwörter für die zwei Zugänge.** Als Hash, nicht als Klartext:
```bash
php -r 'echo password_hash("DEIN-PASSWORT", PASSWORD_DEFAULT), "\n";'
```
Ausgabe (beginnt mit `$2y$…`) in `dateien.users.verein.password_hash` bzw.
`dateien.users.admin.password_hash` eintragen. Zwei verschiedene Passwörter: `verein`
gibt der Verein weiter, `admin` behältst du.
**Lizenzschlüssel** der Files Gallery in `dateien.license_key`. Fehlt er, läuft die
Ablage trotzdem, zeigt aber den Hinweis der unlizenzierten Version (nur eine Warnung
in `preflight`, kein Blocker).
Bereits erledigt: `cron.key` ist auf `''` gesetzt und der Endpoint `public/cron.php`
damit zu. Der Takt kommt ab jetzt aus echten Cronjobs (Teil D), der Endpoint war nur
der Ersatz für den fehlenden Cron.
### A2 [lokal] Alles gepusht?
```bash
git status --porcelain # muss leer sein
git push origin main
```
---
## Teil B — Server einrichten
### B1 [SSH] Umgebung feststellen und notieren
```bash
ssh ssh-w01ca75d@SERVER # SERVER = Hostname oder IP aus dem KAS
pwd # das Verzeichnis, in dem SSH startet
which php; php -v # PHP muss >= 8.1 sein
git --version
```
**Ergebnis vom 31.07.2026 auf diesem Webspace:**
| | |
|---|---|
| Webspace-Wurzel | `/www/htdocs/w01ca75d` |
| SITE | `/www/htdocs/w01ca75d/tsv08kulmbach-website` |
| PHP (CLI, für Cron) | `/usr/bin/php` — 8.3.29 |
| Composer | 2.9.8, global vorhanden |
| Alte CMS-Seite (Rückweg) | `/www/htdocs/w01ca75d/tsv08kulmbach.de` |
**Falle: `~` ist hier `/`, nicht der Webspace.** Das SSH-Konto hat die Systemwurzel
als Home-Verzeichnis, `cd ~` führt also aus dem Webspace heraus und `~/…`-Pfade
existieren nicht. Alle Pfade in dieser Anleitung sind deshalb absolut — auch das
`scp`-Ziel und die Cron-Zeilen.
All-Inkl legt pro Domain einen Ordner unter `/www/htdocs/<user>/` an, dieses
Verzeichnis selbst ist also kein Docroot. Der Ordnername `tsv08kulmbach.de` ist von der alten Seite belegt, deshalb
heißt der neue `tsv08kulmbach-website`. Die anderen TSV-Ordner
(`2024.`, `hall-of-fame.`, `kabinendienst.`, `kreisliga-reise.`) sind eigene
Subdomains und vom Umschalten nicht betroffen.
Achtung: das ist die **CLI**-Version. Welches PHP der Webserver nimmt, stellt KAS pro
Domain getrennt ein (Schritt C1).
### B2 [SSH] Repo klonen
```bash
cd /www/htdocs/w01ca75d
git clone https://onion.breadcrumb-online.de/fs/tsv08kulmbach-website.git tsv08kulmbach-website
cd tsv08kulmbach-website && pwd # diesen Pfad notieren: das ist SITE
```
Der Klon lädt gut 460 MB (Website plus das Vereinsmaterial der Dateiablage), das
dauert ein paar Minuten. Die alte CMS-Seite liegt in einem anderen Verzeichnis und
wird nicht angefasst.
### B3 [SSH] `config.php` platzieren
Die Datei ist gitignored, also nicht im Klon. Vom Mac hochladen:
```bash
# [lokal], in einem zweiten Terminal.
# SERVER = derselbe Host wie beim ssh-Login (hier die Server-IP des Webspace).
scp config/config.production.php \
ssh-w01ca75d@SERVER:/www/htdocs/w01ca75d/tsv08kulmbach-website/config/config.php
```
Platzhalter bewusst ohne spitze Klammern: `<host>` liest die zsh als
Eingabe-Umleitung und bricht mit „no such file or directory" ab.
Wichtig: Ziel heißt `config.php`, nicht `config.production.php`. Danach [SSH]:
```bash
chmod 600 config/config.php # enthält SMTP-Passwort und app_secret
```
### B4 [SSH] Composer-Abhängigkeiten
```bash
cd /www/htdocs/w01ca75d/tsv08kulmbach-website
composer install --no-dev --optimize-autoloader
```
Kennt die Shell `composer` nicht, hol ihn dir lokal ins Verzeichnis:
```bash
curl -sS https://getcomposer.org/installer | php
php composer.phar install --no-dev --optimize-autoloader
rm composer.phar
```
Es wird nur PHPMailer installiert.
### B4b [SSH] Git-Identität für den Server setzen
Ohne sie bricht **jeder** Commit auf dem Server ab („Author identity unknown"),
und ohne Commits funktioniert der Update-Ablauf aus F1 nicht.
```bash
cd /www/htdocs/w01ca75d/tsv08kulmbach-website
git config --local user.name "TSV 08 Server"
git config --local user.email "fs@breadcrumb-solutions.de"
```
`--local` gilt nur für dieses Repo — die anderen Projekte auf dem Webspace bleiben
unberührt. Der Name macht in `git log` auf einen Blick sichtbar, welche Commits vom
Server stammen (Uploads, Sync-Stände) und welche vom Mac (Code, Inhalte).
Wird ein Push mit `! [rejected] … (fetch first)` abgewiesen, hat der Mac inzwischen
gepusht: `git pull --rebase origin main` und nochmal pushen. Rebase statt Merge, damit
die Historie linear bleibt.
### B5 [SSH] Schreibrechte prüfen
PHP läuft unter deinem Benutzer, meist passt es also schon. `preflight` sagt es dir
in Teil C4 verbindlich. Falls dort etwas fehlt:
```bash
chmod -R u+rwX storage
```
Schreiben muss die Seite in `storage/logs`, `storage/cache`, `storage/ratelimit`,
`storage/dateien/uploads` und `storage/dateien/system`.
---
## Teil C — Auf der Test-Subdomain prüfen
### C1 [KAS] Subdomain anlegen und schützen
1. **Domains** → Subdomain `neu.tsv08kulmbach.de` anlegen.
2. Als Verzeichnis **`/tsv08kulmbach-website/public`** eintragen, nicht das Repo-Root.
Damit liegen `app/`, `config/`, `data/`, `storage/` und `vendor/` außerhalb des
Docroots und sind grundsätzlich nicht per URL erreichbar. (Die Root-`.htaccess`
ist nur ein Notnagel für Hoster, die das nicht können.)
3. **PHP-Version** für die Subdomain auf 8.1 oder neuer stellen.
4. **SSL** aktivieren (Let's Encrypt). Ohne Zertifikat läuft die Seite in eine
Endlosweiterleitung, denn `public/.htaccess` erzwingt HTTPS.
5. **Verzeichnisschutz** (Basic Auth) — empfohlen, aber kein Blocker. Er hält die
Testseite komplett aus dem Netz. Ohne ihn ist das Risiko klein, weil jede Seite
ein Canonical auf `www.tsv08kulmbach.de` trägt: Google konsolidiert darauf,
doppelter Inhalt entsteht praktisch nicht. Es kann lediglich die Test-URL bis zum
Umschalten in einem Suchergebnis auftauchen. Am 31.07.2026 bewusst weggelassen.
Wer es ohne Basic Auth sauber will: `X-Robots-Tag: noindex` in
`public/.htaccess`, host-bedingt nur für Namen, die mit `neu.` beginnen.
### C2 Erreichbarkeit
`https://neu.tsv08kulmbach.de` aufrufen. Nach der Passwortabfrage muss die
Startseite kommen. Weißes Blatt oder 500er? Dann [SSH]
`tail -30 storage/logs/php-errors.log`.
### C3 [SSH] Datenstände erzeugen
`data/matchcenter.json` und `data/instagram.json` sind bewusst nicht im Repo, die
holen die Syncs. Vor dem ersten Lauf fehlen Tabellen, Spiele und der
Instagram-Block (die Seite bricht deswegen nicht, sie zeigt Ersatzinhalte).
```bash
cd /www/htdocs/w01ca75d/tsv08kulmbach-website
php bin/matchcenter-sync.php # Spiele, Ergebnisse, Tabellen, Wappen
php bin/instagram-sync.php # Posts und Bilder
```
**Der Instagram-Lauf ist der Wackelkandidat des ganzen Deploys.** Instagram sperrt
Rechenzentrums-IPs; bisher lief der Scraper nur über einen privaten Anschluss.
Schlägt er hier fehl, ist das kein Fehler im Deploy: dann bleibt der Weg, ihn von
zu Hause laufen zu lassen und `data/instagram.json` samt Bildern hochzuladen, oder
der Block wird redaktionell gepflegt. Die Website funktioniert in jedem Fall.
### C4 [SSH] `preflight` bis null Fehler
```bash
php bin/preflight.php; echo "Exit: $?"
```
Ziel ist **Exit 0**. Warnungen darfst du bewerten, Fehler nicht. Erwartbare
Warnungen hier: der Hinweis, dass `cron.key` leer ist (richtig so, siehe A1) und
die Uploadgrenze, die erst ein echter Upload beweist (C6).
### C5 Durchklicken
Die Seiten, die eigene Datenquellen haben:
- [ ] Startseite: Banner-Slider, Termin-Karte im Hero, Instagram-Block
- [ ] `/matchcenter`: Tabellen und nächste Spiele mit Wappen
- [ ] `/termine`, `/news`, `/veranstaltungen` plus eine Detailseite
- [ ] `/stadionzeitung` und die Ausgabe durchswipen — **alle Wappen und
Instagram-Bilder da?** (Sie liegen im Repo, sollten also stimmen.)
- [ ] `/stadionzeitung/aktuell` landet auf der neuesten Ausgabe
- [ ] Die Druckfassung einer Ausgabe (`?druck=1`)
- [ ] `/impressum`: Webadresse zeigt `www.tsv08kulmbach.de`, nicht die Subdomain
### C6 Dateiablage und Mailversand
```bash
php bin/dateien-init.php # Jahr/Abteilung-Ordner, MUSS auf dem Server laufen
php bin/smtp-test.php # nur Verbindung und Login, kein Versand
```
`dateien-init.php` gehört auf den Server, weil Umlaute per FTP zu Doppelordnern
führen (macOS NFD gegen Linux NFC).
- [ ] `/dateien` mit beiden Zugängen anmelden
- [ ] Eine große Datei hochladen (>10 MB) — beweist die 250-MB-Grenze
- [ ] Ein Formular abschicken (z. B. `/kontakt`) und die Mail im Postfach prüfen
- [ ] `php bin/logs.php` zeigt keine Fehler
---
## Teil D — Cronjobs
**Die KAS-Cronjobs können nur URLs aufrufen, keine Shell-Befehle** (geprüft 31.07.2026:
das Formular hat ein Feld „Protokoll / Pfad" mit `https://`, kein Kommandofeld). Der
Takt läuft deshalb über `public/cron.php`. Aufrufer ist der Hoster selbst — der
Schlüssel verlässt den Webspace nie, kein Dritter ist beteiligt.
Voraussetzung: `cron.key` muss in `config/config.php` gesetzt sein (48 Zeichen,
nicht `app_secret`). Steht er leer, antwortet der Endpoint mit 403 und **nichts** synct.
`bin/preflight.php` prüft Länge und Verwechslung.
[KAS] → **Cronjobs** → Anlegen, drei bis vier Einträge. `KEY` durch den Wert aus
`config.php` ersetzen:
| # | Pfad | Zeitpunkt |
|---|---|---|
| 1 | `www.tsv08kulmbach.de/cron.php?job=matchcenter&key=KEY` | stündlich, Minute 37 |
| 2 | `www.tsv08kulmbach.de/cron.php?job=instagram&key=KEY` | täglich, 06:17 |
| 3 | `www.tsv08kulmbach.de/cron.php?job=instagram&key=KEY` | täglich, 18:17 |
| 4 | `www.tsv08kulmbach.de/cron.php?job=logrotate&key=KEY` | täglich, 03:23 |
Vier Dinge dahinter sind Absicht:
- **Matchcenter stündlich statt halbstündlich am Spieltag.** Das KAS kann Wochentage
nicht mit einem Halbstunden-Takt kombinieren, wie es die ursprüngliche Crontab vorsah.
Stündlich rund um die Uhr ist der brauchbare Ersatz: am Spieltag steht das Ergebnis
spätestens eine Stunde später. Kostet 24 Läufe/Tag. Wer den Verkehr zum BFV klein
halten will, nimmt zwei „täglich"-Jobs (07:37 und 19:37) und lebt damit, dass
Samstagsergebnisse erst abends stehen.
- **Instagram zwei Einträge**, weil „täglich" nur einen Zeitpunkt kennt. Bewusst nicht
stündlich: Instagram sperrt auffällige Zugriffsmuster schneller als der BFV.
- **Versetzte Minuten**, damit sich zwei Läufe nie überlappen (die Syncs sperren sich
per Lock, sonst fällt einer aus).
- **Das E-Mail-Feld im KAS bleibt leer.** Die Antwort ist immer leer (204 = gelaufen oder
wegen Mindestabstand übersprungen, 403 = Schlüssel oder Job falsch), es gäbe also
nichts zu mailen. Ob der Takt läuft, sagt `php bin/logs.php`; `php bin/preflight.php`
warnt, wenn ein Datenstand zu alt wird.
**Am Tag nach dem ersten Lauf** einmal `php bin/logs.php` ansehen. Läuft nichts, ist fast
immer der Schlüssel in der URL falsch oder abgeschnitten. Gegenprobe von Hand:
```bash
curl -s -o /dev/null -w '%{http_code}\n' \
'https://www.tsv08kulmbach.de/cron.php?job=matchcenter&key=KEY'
```
204 = angenommen, 403 = Schlüssel oder Job falsch. Direkt danach ein zweites Mal
aufgerufen muss es ebenfalls 204 geben — dann hat der Mindestabstand gegriffen und der
Lauf wurde übersprungen, was im Log als WARN steht.
## Teil E — Umschalten auf die Hauptdomain
Ab hier ist die Seite öffentlich. Alles davor muss sitzen.
### E1 Sicherheitsnetz
Notiere, auf welches Verzeichnis `www.tsv08kulmbach.de` **jetzt** zeigt (die alte
CMS-Seite). Das ist dein Rückweg. Verzeichnis und Datenbank der alten Seite bleiben
unangetastet, nicht löschen.
### E2 [KAS] Domain umstellen
**Domains**`tsv08kulmbach.de` und `www.tsv08kulmbach.de` auf
**`/tsv08kulmbach-website/public`** zeigen lassen. PHP-Version prüfen, SSL muss für beide
aktiv sein.
### E3 Sofort danach prüfen
- [ ] `https://www.tsv08kulmbach.de` lädt
- [ ] `http://tsv08kulmbach.de` (ohne www, ohne s) landet per 301 auf
`https://www.tsv08kulmbach.de`
- [ ] `/sitemap.xml` und `/robots.txt` erreichbar
- [ ] `curl -sI https://www.tsv08kulmbach.de | head` — Statuszeile 200, kein
Redirect-Loop
- [ ] Ein Formular live abschicken
- [ ] **Einen gedruckten QR-Code mit dem Handy scannen.** Der Code der
Stadionzeitung zeigt auf `www.tsv08kulmbach.de/stadionzeitung/aktuell`.
Scannen ist die einzige echte Prüfung: die QR-Bibliothek arbeitet nicht
reproduzierbar, ein Bildvergleich beweist nichts.
### E4 Verzeichnisschutz der Test-Subdomain lassen
Nicht entfernen. Sonst steht die Seite zweimal im Netz und Google sieht doppelten
Inhalt. Wenn du die Subdomain nicht mehr brauchst, in KAS löschen.
---
## Teil F — Danach
### F1 Der Update-Ablauf ab jetzt
**Die Reihenfolge ist nicht verhandelbar**, weil `storage/dateien/uploads/` im Repo
liegt und andere Menschen dort hochladen:
```bash
# 1. [SSH] auf dem Server: neue Uploads sichern
cd /www/htdocs/w01ca75d/tsv08kulmbach-website
git add -A storage/dateien/uploads && git commit -m "Uploads vom Server" && git push
# 2. [lokal] holen, dann entwickeln
git pull
# 3. [lokal] fertig, gepusht → [SSH] ausrollen
cd /www/htdocs/w01ca75d/tsv08kulmbach-website && git pull && php bin/preflight.php
```
Nie in der anderen Reihenfolge. Details und Begründung in CLAUDE.md, Abschnitt
Dateiablage.
Ändert sich `composer.json`, gehört ein `composer install --no-dev
--optimize-autoloader` dazu. Neue Formularfelder heißen: Datenschutzerklärung
mitziehen.
### F2 Rückweg, falls etwas Grundlegendes bricht
[KAS] Domain zurück auf das alte Verzeichnis aus E1. Nach ein paar Minuten ist die
alte Seite wieder da. Kein Datenverlust, weil die neue Seite in einem eigenen
Verzeichnis liegt und die alte Datenbank nicht anfasst.
### F3 Offen, nach dem Livegang
- ~~**301-Weiterleitungen der alten URLs.**~~ Erledigt am 31.07.2026: `public/.htaccess`,
Block „Alte CMS-Adressen". Abgedeckt sind der PDF-Aufnahmeantrag der Turnabteilung
(`/userdata/01_Basis/Beitrittserkl…pdf`, so in den Einladungen verschickt) und alle
Seiten der alten Navigation unter `/08/de/…` (Zuordnung aus `main_navigation` im
DB-Dump). **Nicht** dabei und bewusst nicht nachgezogen: die Bilder und das
Hero-Video unter `/userdata/04_Seiteninhalte/` — die liegen auf der neuen Seite in
anderer Form, eine Weiterleitung von Bild-URLs hilft niemandem.
- **Google Search Console:** Property anlegen, `sitemap.xml` einreichen.
- **Instagram-Sync** beobachten, siehe C3.
- Nach dem Sportfest (ab 07.09.2026) fallen zwei Anzeigen per `bis`-Datum aus dem
Bestand, die Stadionzeitung hat dann 38 Seiten statt 40 und ist nicht mehr durch
4 teilbar. `bin/stadionzeitung-add.php` warnt beim nächsten Lauf.