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
389 lines
16 KiB
Markdown
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.
|