bin/instagram-sync.php lieferte seit dem 03.09.2026 bei jedem Lauf "Keine Posts ermittelbar". Von einem privaten Anschluss gegengeprueft: feed/user und web_profile_info antworten mit 401 require_login, das Profil-HTML traegt keine Beitragsdaten mehr, auch /embed/ liefert nur noch eine App-Huelle. Es lag also nicht an der Server-IP, und der Scraper ist geloescht, damit ihn niemand wiederbelebt. Graph API (Meta-Konto noetig) und kommerzielle Dienste wurden verworfen. Neu: bin/instagram-add.php. --profil liest die Beitragsliste ueber headless Chrome aus dem gerenderten Profil-DOM (im HTML stehen die Codes nicht) und holt zu jedem neuen Beitrag Bild, Caption und Datum ueber Instagrams oEmbed-Endpunkt; das Datum steckt in der media_id. Daneben einzelne URLs, --liste, --entfernen und --bild als Notfall. Der Shortcode ist Primaerschluessel und damit die komplette Dedupe-Logik. Die Struktur von data/instagram.json bleibt exakt gleich, deshalb war an Startseite, News-Feed und Stadionzeitung nichts zu aendern. Kein Cron mehr: der Job faellt aus public/cron.php, auf dem Webspace gibt es kein Chrome. Die Chrome-Suche liegt jetzt gemeinsam in bin/chrome-lib.php, genutzt von bin/logo-build.php und bin/instagram-add.php. data/instagram.json ist ab jetzt versioniert. Solange sie ignoriert war, lief sie gegen die versionierten Bilder auseinander: lokal lagen 10 Bilder zu 9 Eintraegen, drei Beitraege fehlten im Feed, zwei Eintraege hatten kein Bild. Repariert, jetzt 16 Eintraege zu 16 Bildern. bin/preflight.php prueft das in beide Richtungen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J9mubC8gtpjtJntbhSJBRS
16 KiB
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:
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?
git status --porcelain # muss leer sein
git push origin main
Teil B — Server einrichten
B1 [SSH] Umgebung feststellen und notieren
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
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:
# [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]:
chmod 600 config/config.php # enthält SMTP-Passwort und app_secret
B4 [SSH] Composer-Abhängigkeiten
cd /www/htdocs/w01ca75d/tsv08kulmbach-website
composer install --no-dev --optimize-autoloader
Kennt die Shell composer nicht, hol ihn dir lokal ins Verzeichnis:
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.
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:
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
- Domains → Subdomain
neu.tsv08kulmbach.deanlegen. - Als Verzeichnis
/tsv08kulmbach-website/publiceintragen, nicht das Repo-Root. Damit liegenapp/,config/,data/,storage/undvendor/außerhalb des Docroots und sind grundsätzlich nicht per URL erreichbar. (Die Root-.htaccessist nur ein Notnagel für Hoster, die das nicht können.) - PHP-Version für die Subdomain auf 8.1 oder neuer stellen.
- SSL aktivieren (Let's Encrypt). Ohne Zertifikat läuft die Seite in eine
Endlosweiterleitung, denn
public/.htaccesserzwingt HTTPS. - 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.deträ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: noindexinpublic/.htaccess, host-bedingt nur für Namen, die mitneu.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 ist bewusst nicht im Repo, die holt der Sync. Vor dem
ersten Lauf fehlen Tabellen und Spiele (die Seite bricht deswegen nicht, sie zeigt
Ersatzinhalte).
cd /www/htdocs/w01ca75d/tsv08kulmbach-website
php bin/matchcenter-sync.php # Spiele, Ergebnisse, Tabellen, Wappen
Instagram braucht hier nichts mehr. Seit dem 17.09.2026 gibt es keinen Scraper
und keinen Cron dafür: data/instagram.json liegt IM REPO und kommt mit dem Pull
mit, die Bilder ebenso. Neue Beiträge nimmt Claude im Chat mit
php bin/instagram-add.php <post-url> auf. Hintergrund in CLAUDE.md, Abschnitt
„Instagram-Beiträge".
Einmalig beim ersten Pull nach dem 17.09.2026: auf dem Server liegt noch eine
alte, unversionierte data/instagram.json vom letzten Sync-Lauf. Git bricht den
Pull sonst ab („untracked working tree file would be overwritten"). Sie ist
reiner Skript-Output und enthält keine Pflegearbeit, also einfach löschen:
rm data/instagram.json && git pull.
C4 [SSH] preflight bis null Fehler
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,/veranstaltungenplus eine Detailseite/stadionzeitungund die Ausgabe durchswipen — alle Wappen und Instagram-Bilder da? (Sie liegen im Repo, sollten also stimmen.)/stadionzeitung/aktuelllandet auf der neuesten Ausgabe- Die Druckfassung einer Ausgabe (
?druck=1) /impressum: Webadresse zeigtwww.tsv08kulmbach.de, nicht die Subdomain
C6 Dateiablage und Mailversand
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).
/dateienmit 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.phpzeigt 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=logrotate&key=KEY |
täglich, 03:23 |
Die beiden früheren Instagram-Jobs (06:17 und 18:17) sind am 17.09.2026 entfallen und gehören im KAS gelöscht — der Endpoint kennt den Job nicht mehr und antwortet mit 403.
Drei 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.
- 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.phpwarnt, 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:
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.delädthttp://tsv08kulmbach.de(ohne www, ohne s) landet per 301 aufhttps://www.tsv08kulmbach.de/sitemap.xmlund/robots.txterreichbarcurl -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:
# 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 ausmain_navigationim 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.xmleinreichen. - 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.phpwarnt beim nächsten Lauf.