Den Twitch-Streamplan auf der eigenen Website anzeigen — ohne Plugin

·

Auf unserer Stream-Seite zeigen wir, wann wir als Nächstes live sind. Die Termine pflegen wir genau einmal — direkt bei Twitch. WordPress holt sie sich dann automatisch und rendert sie im Brand-Look. Kein zweites Kalendersystem, kein Copy-Paste, keine vergessenen Updates. In diesem Tutorial zeig ich dir, wie das geht.

Warum nicht einfach ein Plugin?

Es gibt ein paar WordPress-Plugins, die Twitch-Daten einbinden. Die meisten machen zu viel (Live-Player, Chat-Widget, Follower-Anzeige, alles auf einmal) oder zu wenig (letzte VODs, aber kein Schedule). Für unseren Fall brauchten wir exakt eins: die Streamtermine der nächsten Tage, als saubere Liste, im Tulori-Look.

Zwei dünne PHP-Funktionen plus ein Shortcode sind am Ende weniger Code als das Konfigurations-Panel eines Plugins. Und wenn sich Twitchs API mal ändert, sind wir nicht auf einen Plugin-Autor angewiesen, der noch Zeit dafür findet.

Was wir bauen

Am Ende dieses Tutorials hast du einen Shortcode [tulori_stream_schedule], den du in jede Seite oder jeden Beitrag einbauen kannst. Er zeigt automatisch die nächsten fünf Stream-Termine aus deinem Twitch-Kalender:

  • Datum auf Deutsch (Fr, 24. Apr · 20:00 Uhr)
  • Titel und Kategorie des Streams
  • Caching, damit die Twitch-API nicht bei jedem Seitenaufruf behelligt wird
  • Sauberes Fehlerverhalten, falls die API mal streikt

Wir nutzen dafür die offizielle Helix-API von Twitch. Kein Scraping, keine inoffiziellen Tricks.

Voraussetzungen

  • Ein Twitch-Account mit Streams (oder zumindest geplanten Terminen im Kalender)
  • Zwei-Faktor-Authentifizierung (2FA) bei Twitch aktiviert — ohne die kannst du keine Developer-App registrieren
  • Zugriff auf deine wp-config.php und die functions.php deines (Child-)Themes
  • Eine laufende Kaffeekanne — der schwierigste Teil ist nicht der Code

Schritt 1: Twitch Developer App registrieren

Jede Anfrage an die Twitch-API muss sich ausweisen. Dafür registrieren wir eine App und bekommen ein Paar aus Client ID (öffentlich) und Client Secret (privat).

  1. Geh auf dev.twitch.tv/console/apps und logg dich ein
  2. Klick auf „Register Your Application“
  3. Name: Etwas Eindeutiges, z.B. „Meine Website Schedule“
  4. OAuth Redirect URL: http://localhost — wir nutzen die nicht, aber das Feld verlangt was
  5. Category: Website Integration
  6. Client Type: Confidential
  7. Speichern → auf die App klicken → „New Secret“ generieren

Das Secret wird nur einmal angezeigt. Kopier es sofort in deinen Passwort-Manager. Falls du es verlierst, generierst du ein neues — das alte wird automatisch ungültig.

Schritt 2: Credentials in wp-config.php

Das Secret gehört nicht in den Theme-Code. Wenn du dein Theme mal in Git pusht oder jemandem zuschickst, wäre es sofort öffentlich. Die wp-config.php ist der richtige Ort für solche Werte.

Öffne die wp-config.php und füge die drei Zeilen zwischen diesen beiden WordPress-Kommentaren ein:

/* Add any custom values between this line and the "stop editing" line. */

/* Twitch API Credentials */
define( 'TULORI_TWITCH_CLIENT_ID',     'DEINE_CLIENT_ID' );
define( 'TULORI_TWITCH_CLIENT_SECRET', 'DEIN_SECRET' );
define( 'TULORI_TWITCH_CHANNEL_LOGIN', 'dein_twitch_username' );

/* That's all, stop editing! Happy publishing. */

Bei TULORI_TWITCH_CHANNEL_LOGIN gibst du deinen Twitch-Usernamen ein (bei uns ist das balduur). Nicht die Anzeige-Variante mit Großbuchstaben, sondern den Lowercase-Login.

Schritt 3: Der PHP-Code

Das Herzstück kommt in die functions.php deines Themes (am besten Child-Theme, damit’s beim nächsten Theme-Update nicht überschrieben wird).

Der Code besteht aus vier Teilen, die ich einzeln durchgehe, bevor wir sie am Stück einbauen:

3.1 — App-Access-Token holen

Twitch nutzt OAuth 2.0. Wir tauschen unser Client-ID-und-Secret-Paar gegen einen zeitlich begrenzten Access Token, mit dem wir uns dann bei echten API-Calls ausweisen. Der Token gilt ca. 60 Tage — wir cachen ihn 50 Tage, damit wir auf der sicheren Seite sind.

function tulori_twitch_get_access_token() {
    $cached = get_transient( 'tulori_twitch_access_token' );
    if ( $cached ) {
        return $cached;
    }

    if ( ! defined( 'TULORI_TWITCH_CLIENT_ID' ) || ! defined( 'TULORI_TWITCH_CLIENT_SECRET' ) ) {
        return false;
    }

    $response = wp_remote_post( 'https://id.twitch.tv/oauth2/token', array(
        'body' => array(
            'client_id'     => TULORI_TWITCH_CLIENT_ID,
            'client_secret' => TULORI_TWITCH_CLIENT_SECRET,
            'grant_type'    => 'client_credentials',
        ),
        'timeout' => 10,
    ) );

    if ( is_wp_error( $response ) ) {
        return false;
    }

    $body = json_decode( wp_remote_retrieve_body( $response ), true );
    if ( empty( $body['access_token'] ) ) {
        return false;
    }

    set_transient( 'tulori_twitch_access_token', $body['access_token'], 50 * DAY_IN_SECONDS );
    return $body['access_token'];
}

3.2 — Broadcaster-ID aus dem Username holen

Fast alle Twitch-API-Endpoints erwarten keine Usernames, sondern numerische IDs. Wir brauchen also einmal eine Abfrage, die aus balduur die Broadcaster-ID macht. Die ID ändert sich nie — also cachen wir sie einfach 30 Tage.

function tulori_twitch_get_broadcaster_id() {
    $cached = get_transient( 'tulori_twitch_broadcaster_id' );
    if ( $cached ) {
        return $cached;
    }

    $token = tulori_twitch_get_access_token();
    if ( ! $token || ! defined( 'TULORI_TWITCH_CHANNEL_LOGIN' ) ) {
        return false;
    }

    $response = wp_remote_get( 'https://api.twitch.tv/helix/users?login=' . urlencode( TULORI_TWITCH_CHANNEL_LOGIN ), array(
        'headers' => array(
            'Client-ID'     => TULORI_TWITCH_CLIENT_ID,
            'Authorization' => 'Bearer ' . $token,
        ),
        'timeout' => 10,
    ) );

    if ( is_wp_error( $response ) ) {
        return false;
    }

    $body = json_decode( wp_remote_retrieve_body( $response ), true );
    if ( empty( $body['data'][0]['id'] ) ) {
        return false;
    }

    $broadcaster_id = $body['data'][0]['id'];
    set_transient( 'tulori_twitch_broadcaster_id', $broadcaster_id, 30 * DAY_IN_SECONDS );
    return $broadcaster_id;
}

3.3 — Den Schedule holen

Jetzt der eigentliche Aufruf: die nächsten fünf Stream-Termine. Wir cachen 15 Minuten — häufiger muss sich das wirklich nicht aktualisieren, und die Twitch-API bedankt sich.

function tulori_twitch_get_schedule() {
    $cached = get_transient( 'tulori_twitch_schedule' );
    if ( $cached !== false ) {
        return $cached;
    }

    $token          = tulori_twitch_get_access_token();
    $broadcaster_id = tulori_twitch_get_broadcaster_id();
    if ( ! $token || ! $broadcaster_id ) {
        return false;
    }

    $response = wp_remote_get( 'https://api.twitch.tv/helix/schedule?broadcaster_id=' . $broadcaster_id . '&first=5', array(
        'headers' => array(
            'Client-ID'     => TULORI_TWITCH_CLIENT_ID,
            'Authorization' => 'Bearer ' . $token,
        ),
        'timeout' => 10,
    ) );

    if ( is_wp_error( $response ) ) {
        return false;
    }

    $code = wp_remote_retrieve_response_code( $response );

    if ( $code === 404 ) {
        // Kein Schedule angelegt — das ist kein Fehler, nur eine leere Liste.
        set_transient( 'tulori_twitch_schedule', array(), 15 * MINUTE_IN_SECONDS );
        return array();
    }

    if ( $code !== 200 ) {
        return false;
    }

    $body     = json_decode( wp_remote_retrieve_body( $response ), true );
    $segments = isset( $body['data']['segments'] ) ? $body['data']['segments'] : array();

    $schedule = array();
    foreach ( $segments as $seg ) {
        if ( ! empty( $seg['canceled_until'] ) ) {
            continue;
        }
        $schedule[] = array(
            'start_time' => isset( $seg['start_time'] ) ? $seg['start_time'] : null,
            'title'      => isset( $seg['title'] ) ? $seg['title'] : '',
            'category'   => isset( $seg['category']['name'] ) ? $seg['category']['name'] : '',
        );
    }

    set_transient( 'tulori_twitch_schedule', $schedule, 15 * MINUTE_IN_SECONDS );
    return $schedule;
}

Drei Kleinigkeiten, die mir hier wichtig waren:

  • 404 ist kein Fehler. Wenn du noch keinen Schedule angelegt hast, antwortet Twitch mit 404. Das ist aber inhaltlich „keine Termine“, nicht „kaputt“. Wir behandeln das als leere Liste.
  • Abgesagte Termine fliegen raus. Wenn du einen Termin im Twitch-Dashboard als „canceled“ markierst, soll der nicht mehr auftauchen.
  • Unterscheidung === false vs. empty(). Eine leere Liste ist ein gültiger Cache-Wert. Erst wenn false drinsteht, holen wir wirklich neu.

3.4 — Der Shortcode

Die eigentliche Darstellung. Ich baue die Termine als <ul> mit eigenen CSS-Klassen, damit ich im Stylesheet das Design bauen kann, ohne im PHP rumzupfuschen.

function tulori_stream_schedule_shortcode( $atts ) {
    $schedule = tulori_twitch_get_schedule();

    if ( $schedule === false ) {
        return '<p class="tulori-schedule-error">Stream-Termine konnten gerade nicht geladen werden.</p>';
    }

    if ( empty( $schedule ) ) {
        return '<p class="tulori-schedule-empty">Aktuell sind keine Stream-Termine eingetragen.</p>';
    }

    $wochentage = array( 'So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa' );
    $monate     = array( '', 'Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez' );

    $tz = new DateTimeZone( 'Europe/Berlin' );

    $html = '<ul class="tulori-schedule">';
    foreach ( $schedule as $item ) {
        if ( empty( $item['start_time'] ) ) {
            continue;
        }
        $dt = new DateTime( $item['start_time'] );
        $dt->setTimezone( $tz );

        $wochentag = $wochentage[ (int) $dt->format( 'w' ) ];
        $tag       = (int) $dt->format( 'j' );
        $monat     = $monate[ (int) $dt->format( 'n' ) ];
        $uhrzeit   = $dt->format( 'H:i' );

        $title    = ! empty( $item['title'] )    ? esc_html( $item['title'] )    : 'Thema folgt';
        $category = ! empty( $item['category'] ) ? esc_html( $item['category'] ) : '';

        $html .= '<li class="tulori-schedule__item">';
        $html .= '<span class="tulori-schedule__date"><strong>' . esc_html( $wochentag . ', ' . $tag . '. ' . $monat ) . '</strong> · ' . esc_html( $uhrzeit ) . ' Uhr</span>';
        $html .= '<span class="tulori-schedule__title">' . $title . '</span>';
        if ( $category ) {
            $html .= '<span class="tulori-schedule__category">' . $category . '</span>';
        }
        $html .= '</li>';
    }
    $html .= '</ul>';

    return $html;
}
add_shortcode( 'tulori_stream_schedule', 'tulori_stream_schedule_shortcode' );

Eine bewusste Entscheidung: Ich formatiere die Wochentage und Monate selbst statt PHPs strftime() zu nutzen. Letzteres braucht einen deutschen Locale auf dem Server, der je nach Hoster nicht zuverlässig verfügbar ist. Ein Array mit zwölf Einträgen ist robuster als eine Abhängigkeit, die manchmal fehlt.

Schritt 4: Einbauen und CSS

Den Shortcode setzt du einfach dort in deine Seite, wo die Termine erscheinen sollen:

[tulori_stream_schedule]

Für die Optik habe ich ein dezentes CSS drauf, das zum Rest der Seite passt — Datum in serifenloser Schrift, Titel kursiv, Kategorie als uppercase-Label. Die Details variieren von Projekt zu Projekt, deshalb hier nur die Grundstruktur:

.tulori-schedule {
    list-style: none;
    padding: 0;
    margin: 1.5rem 0 0 0;
}

.tulori-schedule__item {
    padding: 1rem 0;
    border-top: 1px solid #ddd;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

.tulori-schedule__item:last-child {
    border-bottom: 1px solid #ddd;
}

.tulori-schedule__title {
    font-style: italic;
}

.tulori-schedule__category {
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    opacity: 0.6;
}

Cache-Reset für Admins

Während der Entwicklung willst du nicht 15 Minuten warten, bis der Cache abläuft, um eine Änderung zu sehen. Eine kleine Admin-Funktion leert ihn auf Zuruf:

function tulori_reset_twitch_cache() {
    if ( isset( $_GET['tulori_reset_twitch'] ) && current_user_can( 'manage_options' ) ) {
        delete_transient( 'tulori_twitch_access_token' );
        delete_transient( 'tulori_twitch_broadcaster_id' );
        delete_transient( 'tulori_twitch_schedule' );
        wp_die( 'Twitch-Cache geleert.' );
    }
}
add_action( 'init', 'tulori_reset_twitch_cache' );

Als eingeloggter Admin rufst du dann deine-domain.de/?tulori_reset_twitch=1 auf und der Cache ist weg. Die current_user_can-Prüfung sorgt dafür, dass das kein Zufallsbesucher triggern kann.

Die Anekdote zum Schluss

Beim ersten Live-Test zeigte die Seite stattdessen stundenlang die Fehlermeldung „Stream-Termine konnten gerade nicht geladen werden“. Cache leeren — gleicher Fehler. Secret nochmal bei Twitch ansehen, kopieren, einsetzen — gleicher Fehler. Code nochmal durchlesen — sieht richtig aus.

Eine Debug-Ausgabe der Twitch-Response brachte es dann zutage:

{„status“:403,“message“:“invalid client secret“}

In der wp-config.php stand beim Secret noch der Platzhalter aus meinem Notiz-Zettel: HIER_DEIN_CLIENT_SECRET_EINSETZEN. Der Text sieht so plausibel aus, dass das Auge ihn beim Drüberlesen einfach für „den echten Kram“ hält. Zwanzig Minuten Lebenszeit für ein Copy-Paste, das nie stattgefunden hatte.

Falls du beim Nachbauen auf invalid client secret stößt: zuerst die wp-config.php öffnen und gucken, ob da wirklich dein Secret drin steht. Nicht der Platzhalter. Ehrlich.

Was du jetzt hast

Einen automatisch synchronisierenden Streamplan auf deiner Website. Pflegst du deine Termine im Twitch Creator Dashboard, tauchen sie innerhalb von 15 Minuten auf der Seite auf. Kein doppeltes Kalendersystem, kein Copy-Paste-Vergessen.

Der gleiche Aufbau — Token holen, ID holen, Daten holen, im Shortcode rendern — funktioniert auch für andere Twitch-Endpoints. VODs auflisten, aktuellen Stream-Status anzeigen, letzte Clips einbinden. Die API-Dokumentation findest du bei dev.twitch.tv/docs/api.

Fragen oder Verbesserungsvorschläge? Schreib uns per Kontaktformular oder kommentier direkt unten drunter.

Schlagwörter

Geschrieben von

Avatar von Lars-Erik Richter