Instagram: Scraper raus, Beitraege einzeln ueber oEmbed aufnehmen
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
This commit is contained in:
79
CLAUDE.md
79
CLAUDE.md
@@ -21,7 +21,7 @@ selbst gehosteter Stack ohne Abhängigkeiten von Drittanbietern.
|
||||
1. **Keine externen Einbindungen.** Keine CDNs, keine externen Fonts/Scripts/iframes/Embeds, keine
|
||||
Tracking-/Analytics-Dienste, kein reCAPTCHA. Alles wird self-hosted. Einzige erlaubte ausgehende
|
||||
Verbindungen: All-Inkl-SMTP (Mailversand, serverseitig) und die CLI/Cron-Skripte unter `bin/`
|
||||
(Instagram-Scraper, Matchcenter-Sync, Files-Gallery-Update — nie im Request-Pfad).
|
||||
(Matchcenter-Sync, Instagram-Aufnahme, Files-Gallery-Update — nie im Request-Pfad).
|
||||
Besucher-Browser kontaktieren ausschließlich unsere Domain. Die CSP (`default-src 'self'`) erzwingt das — nicht aufweichen.
|
||||
2. **`.context/` und `.context_db/` sind reine Referenz** (alte, unsichere CMS-Seite). Von dort werden nur
|
||||
Assets (Bilder/Fonts/Videos), Texte und Styling-Werte extrahiert. **Niemals Code übernehmen, niemals
|
||||
@@ -52,7 +52,7 @@ selbst gehosteter Stack ohne Abhängigkeiten von Drittanbietern.
|
||||
| Ansprechpartner/Personen (geteilte Quelle) | `data/ansprechpartner.json` (per Abteilung auf Team-/Jugend-/Verein-Seiten gezogen) | Namen/Funktionen/Kontakte in Templates duplizieren |
|
||||
| Banner-Slider (Startseite) | `data/banners.json` | Slides in `pages/home.php` oder der Komponente |
|
||||
| Partner/Sponsoren | `data/partners.json` | — |
|
||||
| Instagram-Cache | `data/instagram.json` + `public/assets/img/instagram/` | **maschinenverwaltet von `bin/instagram-sync.php` — nie von Hand editieren** |
|
||||
| Instagram-Beiträge | `data/instagram.json` + `public/assets/img/instagram/` | Aufgenommen ausschließlich mit **`php bin/instagram-add.php <post-url>`** (holt Bild, Text und Datum) — Einträge nie von Hand tippen, Bilder nie von Hand skalieren. **Kein Cron**, chat-gepflegt wie `events.json`. Eintrag und Bild müssen immer zusammen existieren, `bin/preflight.php` prüft beide Richtungen |
|
||||
| Matchcenter (Spiele/Ergebnisse/Tabellen) | `data/matchcenter.json` + `public/assets/img/crests/` | **maschinenverwaltet von `bin/matchcenter-sync.php` — nie von Hand editieren**. Team-/Wettbewerbs-IDs in `config.php` (`matchcenter.teams`) |
|
||||
| Stadionzeitung (Ausgaben-Archiv) | `data/stadionzeitung.json` + `public/assets/img/stadionzeitung/` | **maschinenverwaltet von `bin/stadionzeitung-add.php` + `bin/stadionzeitung-publish.php` (Status/Snapshot) — nie von Hand editieren; einzige Chat-Pflege-Ausnahmen sind `vorwort`, `interview`, `aufruf`, `veranstaltung` (Slug-Verweis auf `veranstaltungen.json`) und `cover` (`teaser` einer Sonderausgabe).** Der Ausgaben-Ordner wird bei jedem Lauf komplett geleert und neu gefüllt |
|
||||
| Sponsoren-Anzeigen (Stadionzeitung) | `data/anzeigen.json` + `public/assets/img/anzeigen/` | Dauerhafter Bestand, **nicht** pro Ausgabe: Motive ändern sich selten, das Heft erscheint oft. `sponsor`, `alt` (optional), `aktiv`, `bis` (optional, Enddatum gegen das Ausgabedatum), `platzierung` (optional, fester Platz auf einer Live-Seite statt im Streu-Pool) und die **Reihenfolge** (= Reihenfolge im Heft) im Chat gepflegt; `base`/`widths` schreibt ausschließlich `bin/anzeige-add.php`. Anzeigen nie in den Ausgaben-Ordner legen |
|
||||
@@ -432,17 +432,69 @@ Besucher keine Aktualisierung, und Fire-and-Forget ist auf Shared Hosting unzuve
|
||||
gewünschten Takt sperrt. Der damals hier notierte Ausstieg („wenn der Tarif je Cronjobs bekommt,
|
||||
`cron.key` leeren") ist am 31.07.2026 genau so eingetreten und vollzogen.
|
||||
|
||||
**Offen:** ob der Instagram-Scraper von der Server-IP überhaupt durchkommt. Instagram sperrt
|
||||
Rechenzentren; hier lief er nur über einen privaten Anschluss. Einmal auf dem Server prüfen. Falls er
|
||||
dort blockt, hilft auch GitHub Actions nicht (noch strenger gesperrt) — dann bleibt ein Lauf von einem
|
||||
privaten Anschluss oder der „Aktuelles"-Block wird redaktionell gepflegt.
|
||||
**Erledigt (17.09.2026):** die Frage, ob der Instagram-Scraper von der Server-IP durchkommt, hat sich
|
||||
anders beantwortet als gedacht — er kommt von **keiner** IP mehr durch, siehe unten. Der Job ist aus
|
||||
der Whitelist entfernt, es sind nur noch zwei.
|
||||
|
||||
## Instagram-Feed (Juicer-Ersatz)
|
||||
## Instagram-Beiträge (Juicer-Ersatz, seit 17.09.2026 ohne Scraper)
|
||||
|
||||
`bin/instagram-sync.php` (nur CLI) scraped das öffentliche Profil @tsv08kulmbach, schreibt
|
||||
`data/instagram.json` atomar und lädt Bilder nach `public/assets/img/instagram/`. Bei Fehlern bleibt
|
||||
der letzte gute Cache unangetastet (Seite degradiert, bricht nie; ohne Cache rendert die Komponente
|
||||
eine „Folge uns"-CTA-Karte). Cron: 2×/Tag. Besucher laden Instagram-Inhalte ausschließlich von unserer Domain.
|
||||
**`php bin/instagram-add.php --profil`** (nur CLI) ist der Normalfall: liest die Beitragsliste vom
|
||||
Profil und nimmt alles auf, was noch fehlt. Je Beitrag holt es über Instagrams oEmbed-Endpunkt Bild,
|
||||
Caption und Datum, bringt das Bild auf 640×800 (mittiger Crop auf 4:5) und schreibt
|
||||
`data/instagram.json` atomar. Daneben einzelne Post-URLs als Argument, `--liste=<datei>`,
|
||||
`--entfernen=<shortcode>` und `--bild=<pfad>` (Notfall). Besucher laden Instagram-Inhalte weiterhin
|
||||
ausschließlich von unserer Domain; ohne Einträge rendert die Komponente eine „Folge uns"-CTA-Karte.
|
||||
|
||||
**Die Beitragsliste kommt aus headless Chrome, und das ist der Kern des Ganzen.** Im ausgelieferten
|
||||
Profil-HTML stehen die Beitrags-Codes NICHT — sie entstehen erst durch das JavaScript der Seite.
|
||||
Ein echter Browser bekommt sie ausgeloggt weiterhin (nachgewiesen 17.09.2026: zwölf Codes, darunter
|
||||
vier, die dem Feed fehlten). Das Skript liest sie per `--dump-dom` aus dem gerenderten DOM.
|
||||
Den GraphQL-Aufruf, den der Browser dafür macht, **bewusst nicht nachbauen**: er ist mit
|
||||
Sitzungs-Token abgesichert, und die Rekonstruktion wäre genau die Bastelei, die beim nächsten
|
||||
Meta-Update wieder bricht. `bin/chrome-lib.php` hält `chrome_binary()` und `chrome_run()` für die
|
||||
zwei Skripte, die einen Browser brauchen (hier und `bin/logo-build.php`) — **kein `--user-data-dir`**,
|
||||
siehe Kommentar dort.
|
||||
|
||||
**Kein Cron, und der Grund ist hart:** auf dem All-Inkl-Webspace gibt es kein Chrome. `--profil`
|
||||
läuft nur lokal auf einem Arbeitsrechner, die Aktualisierung braucht also einen Anstoß im Chat
|
||||
(„hol die neuen Instagram-Beiträge"). Dafür sind es zwei Wörter statt gesammelter Links, und die
|
||||
Seite hängt an nichts mehr, das unbemerkt von außen kaputtgehen kann.
|
||||
|
||||
**Warum der Scraper weg ist.** `bin/instagram-sync.php` holte die letzten Beiträge des öffentlichen
|
||||
Profils und lieferte seit dem **03.09.2026** bei jedem Lauf „Keine Posts ermittelbar". Am 17.09.2026
|
||||
von einem **privaten** Anschluss gegengeprüft, alle drei Strategien: `feed/user` und
|
||||
`web_profile_info` antworten mit **401 `require_login`** (`igweb_rollout`), das Profil-HTML enthält
|
||||
keine Beitragsdaten mehr, `?__a=1` ist tot, und auch `/embed/` liefert nur noch eine App-Hülle, die
|
||||
ihren Inhalt erst im Browser nachlädt. **Es lag also nicht an der Server-IP** — ein Lauf von woanders
|
||||
hilft nicht, und der Scraper ist gelöscht, damit ihn niemand wiederbelebt.
|
||||
Verworfene Alternativen: die offizielle **Graph API** (setzt Professional-Konto, Meta-App und Token
|
||||
voraus, abgelehnt von Felix) und **kommerzielle Scraping-Dienste** (Dritter, Kosten, bricht beim
|
||||
nächsten Meta-Update).
|
||||
|
||||
**Was noch geht, und warum es trägt:** `https://www.instagram.com/api/v1/oembed/?url=<post-url>`
|
||||
antwortet ohne Token und liefert `thumbnail_url` (640×800, also genau unser Format), `title` (die
|
||||
komplette Caption) und `media_id`. **Das Datum steckt in der media_id**: `(id >> 23) + 1314220021721`
|
||||
ergibt Millisekunden seit der Instagram-Epoche. Gegen alle neun echten `taken_at`-Werte gerechnet:
|
||||
sechs exakt, drei mit 2 bis 8 Minuten Abweichung (Aufnahme- gegen Veröffentlichungszeit), sichtbar
|
||||
ist ohnehin nur der Tag. **Der Shortcode ist der Primärschlüssel** und damit die komplette
|
||||
Dedupe-Logik: er ist der Dateiname des Bildes und der Schlüssel des Eintrags, ein zweimal
|
||||
geschickter Link aktualisiert statt zu verdoppeln.
|
||||
|
||||
**Die Datenstruktur bleibt exakt wie vorher** (sieben Felder je Beitrag) — deshalb war an
|
||||
`app/components/instagram-feed.php`, `news_build_feed()`, der Momente-Seite der Stadionzeitung und
|
||||
`bin/stadionzeitung-publish.php` **nichts** zu ändern.
|
||||
|
||||
**`data/instagram.json` ist seit dem 17.09.2026 versioniert**, vorher gitignored. Sie ist nicht mehr
|
||||
maschinenverwaltet, ihr Inhalt ist Pflegearbeit. Solange sie ignoriert war, lief sie gegen die
|
||||
versionierten Bilder auseinander: am 17.09.2026 lagen lokal **10 Bilder zu 9 Einträgen** — drei
|
||||
Beiträge vom Server fehlten im lokalen Feed, zwei Einträge hatten kein Bild mehr. `bin/preflight.php`
|
||||
prüft das jetzt in beide Richtungen (Eintrag ohne Bild = Fehler, Bild ohne Eintrag = Warnung).
|
||||
|
||||
**Ehrliche Grenze:** der genutzte Endpunkt ist Instagrams eigener, nicht der offiziell dokumentierte
|
||||
(`graph.facebook.com/instagram_oembed` antwortet zwar auch ohne Token, liefert aber **kein** Bild,
|
||||
nur den Einbettungscode, den Regel 1 verbietet). Fällt er weg, bleibt alles Eingetragene liegen und
|
||||
nichts bricht rückwirkend; für neue Beiträge greift dann `--bild=<pfad>`. Das ist der Unterschied zum
|
||||
Scraper, dessen Ausfall den ganzen Block eingefroren hat.
|
||||
|
||||
## Matchcenter (BFV-Widget-Ersatz)
|
||||
|
||||
@@ -610,7 +662,8 @@ Konflikte mitten in eingefrorenen Datensätzen.
|
||||
alles, worauf er zeigt (`img/crests/`, `img/instagram/`, `img/news/`), als `fix-*` in den
|
||||
Ausgaben-Ordner und schreibt die Pfade um. **Grund:** vorher fror nur die Daten ein, die Pfade
|
||||
zeigten weiter in Ordner, die die Sync-Skripte verwalten — `matchcenter-sync` räumt Wappen weg,
|
||||
die der BFV nicht mehr führt (eine Saison), `instagram-sync` ältere Posts (Wochen). Am 31.07.2026
|
||||
die der BFV nicht mehr führt (eine Saison), und der damalige `instagram-sync` ältere Posts (Wochen;
|
||||
dieses Skript gibt es seit dem 17.09.2026 nicht mehr, das Einfrieren bleibt trotzdem richtig). Am 31.07.2026
|
||||
nachgewiesen: ein Lauf löschte 18 Wappen und 3 Bilder. Eine archivierte Ausgabe wäre also von
|
||||
selbst zerfallen. `img/news` ist mit dabei, weil ein Artikelbild im Chat ersetzt werden kann.
|
||||
**Varianten-Sets** (`{base, widths}`) werden über alle Breiten kopiert und nur umgeschrieben, wenn
|
||||
@@ -1074,7 +1127,7 @@ zieht seine News aus `news_build_feed()` und friert sie beim Veröffentlichen ei
|
||||
Stelle, die sich nicht zurücknehmen lässt.
|
||||
|
||||
**Meldungen werden nicht gekappt.** `config('news.max_items')` begrenzt in `news_build_feed()` nur
|
||||
Instagram und Ergebnisse — die wachsen von allein (Sync 2×/Tag, jedes Wochenende neue Spiele), und
|
||||
Instagram und Ergebnisse — die wachsen von allein (jedes Wochenende neue Spiele), und
|
||||
davor schützt die Grenze. Redaktionelle Meldungen stehen immer alle im Feed, sonst wäre ein älterer
|
||||
Artikel irgendwann von der Website aus nicht mehr verlinkt. **Es gibt bewusst keinen zweiten
|
||||
Archiv-Abschnitt** dafür (30.07.2026 gebaut und am selben Tag verworfen): bei wenigen Artikeln pro
|
||||
|
||||
Reference in New Issue
Block a user