Viele Entwickler hielten RSS nach der Einstellung des Google Readers für ein Relikt der Nullerjahre. In modernen Web-Architekturen ist Syndizierung jedoch relevanter denn je. RSS 2.0, Atom und JSON Feed bilden das Rückgrat automatisierter Content-Pipelines: Sie beliefern Newsletter-Plattformen von Mailchimp bis Brevo, füttern Aggregatoren, synchronisieren Daten mit Social-Media-Schedulern und übertragen Podcast-Episoden an Apple Podcasts oder Spotify.
WordPress bringt seit Version 1.5 eine integrierte Feed-Engine mit. Standardmäßig arbeitet diese Engine unter /feed/ jedoch mit minimaler Konfiguration: ohne Beitragsbilder, beschränkt auf herkömmliche Blog-Beiträge und ohne Mechanismen gegen Content-Scraper. Für professionelle Publisher reicht dieser Standardzustand nicht aus. Dieser Entwickler-Leitfaden zeigt, wie Sie WordPress-Feeds modular anpassen, performance-optimiert ausliefern und um moderne Standards erweitern.
1. Beitragsbilder in RSS-Feeds einbinden: Inline versus Enclosure
Standardmäßige WordPress-Feeds (wp-includes/feed-rss2.php) übergeben im Element <description> oder <content:encoded> lediglich unformatierten Text oder Standard-HTML ohne Beitragsbild (Featured Image). E-Mail-Marketing-Tools, die Kampagnen per RSS-to-Email automatisieren, benötigen jedoch eine feste Bild-URL.
Dafür existieren zwei technische Wege: die Einbettung eines HTML-Bildtags in den Fließtext oder die Ausgabe nativer XML-Elemente im Channel-Item.
Variante A: Bild als HTML vor den Beitragsinhalt stellen
Wenn Ihr Newsletter-Dienst den Text direkt aus dem Feed parst, ist ein Hook in die Filter the_excerpt_rss und the_content_feed die verlässlichste Lösung.
/**
* Bindet das Beitragsbild als HTML in den Feed-Inhalt ein.
*
* @param string $content Der formatierte Inhalt oder Auszug.
* @return string Der modifizierte Inhalt mit Bild.
*/
function wppoland_prepend_rss_thumbnail( string $content ): string {
global $post;
if ( ! ( $post instanceof \WP_Post ) || ! has_post_thumbnail( $post->ID ) ) {
return $content;
}
$thumbnail_id = get_post_thumbnail_id( $post->ID );
$image_src = wp_get_attachment_image_src( $thumbnail_id, 'large' );
if ( ! $image_src || empty( $image_src[0] ) ) {
return $content;
}
$image_url = esc_url( $image_src[0] );
$image_width = absint( $image_src[1] );
$image_height = absint( $image_src[2] );
$image_alt = esc_attr( get_post_meta( $thumbnail_id, '_wp_attachment_image_alt', true ) );
$image_tag = sprintf(
'<p><img src="%s" width="%d" height="%d" alt="%s" class="webfeedsFeaturedVisual" style="display:block;margin-bottom:15px;max-width:100%%;height:auto;" /></p>',
$image_url,
$image_width,
$image_height,
$image_alt
);
return $image_tag . $content;
}
add_filter( 'the_excerpt_rss', 'wppoland_prepend_rss_thumbnail' );
add_filter( 'the_content_feed', 'wppoland_prepend_rss_thumbnail' );Variante B: Standardkonformes Media RSS (media:content) und Enclosure
Für Feed-Reader wie Feedly, Inoreader oder spezialisierte Portale ist die semantische XML-Auszeichnung über den Media-RSS-Namespace (xmlns:media="http://search.yahoo.com/mrss/") sauberer als HTML-Tags im Text.
Hierfür registrieren Sie den Namespace im Feed-Header und fügen das Tag im Item-Loop ein:
/**
* Registriert den Yahoo Media Namespace im RSS 2.0 Header.
*/
function wppoland_register_media_namespace(): void {
echo 'xmlns:media="http://search.yahoo.com/mrss/"' . "\n";
}
add_action( 'rss2_ns', 'wppoland_register_media_namespace' );
/**
* Gibt media:content-Elemente für das Beitragsbild im Feed aus.
*/
function wppoland_append_media_enclosure(): void {
global $post;
if ( ! ( $post instanceof \WP_Post ) || ! has_post_thumbnail( $post->ID ) ) {
return;
}
$thumbnail_id = get_post_thumbnail_id( $post->ID );
$image_src = wp_get_attachment_image_src( $thumbnail_id, 'full' );
if ( ! $image_src || empty( $image_src[0] ) ) {
return;
}
$mime_type = get_post_mime_type( $thumbnail_id ) ?: 'image/jpeg';
printf(
"\t" . '<media:content url="%s" medium="image" type="%s" width="%d" height="%d">' . "\n",
esc_url( $image_src[0] ),
esc_attr( $mime_type ),
absint( $image_src[1] ),
absint( $image_src[2] )
);
printf( "\t\t" . '<media:title type="plain">%s</media:title>' . "\n", esc_html( get_the_title( $post->ID ) ) );
echo "\t" . '</media:content>' . "\n";
}
add_action( 'rss2_item', 'wppoland_append_media_enclosure' );2. Feed-Verzögerung als Schutz vor Scraper-Netzwerken
Ein bekanntes Phänomen bei Websites mit hoher thematischer Autorität: Aggregator-Netzwerke überwachen den Feed im Minutentakt. Sobald ein neuer Artikel erscheint, zieht ein Scraper den Volltext ab und publiziert ihn auf automatisierten Domains.
Wenn der Googlebot die Scraper-Domain früher crawlt als Ihr eigenes Original, kann es vorübergehend zu Fehlinterpretationen bei der Originalquelle kommen. Obwohl Google den Canonical Link meist nachträglich korrigiert, führt dieser Verzug zu Ranking-Schwankungen in Google Discover und den organischen Suchergebnissen.
Eine kontrollierte Verzögerung des RSS-Feeds um 30 bis 60 Minuten gibt Suchmaschinen Zeit, Ihren Originalbeitrag zuerst zu crawlen und im Index zu verankern, bevor externe Feeds darauf zugreifen.
/**
* Verzögert das Erscheinen neuer Beiträge im Hauptfeed um eine Stunde.
*
* @param string $where Die WHERE-Klausel der SQL-Abfrage.
* @param \WP_Query $query Das ausführende WP_Query-Objekt.
* @return string Die modifizierte WHERE-Klausel.
*/
function wppoland_delay_feed_posts( string $where, \WP_Query $query ): string {
global $wpdb;
if ( ! is_admin() && $query->is_feed() && $query->is_main_query() ) {
$delay_minutes = 60;
$now_gmt = current_time( 'mysql', 1 );
$where .= $wpdb->prepare(
" AND TIMESTAMPDIFF(MINUTE, {$wpdb->posts}.post_date_gmt, %s) >= %d ",
$now_gmt,
$delay_minutes
);
}
return $where;
}
add_filter( 'posts_where', 'wppoland_delay_feed_posts', 10, 2 );Wichtig: Diese Modifikation betrifft ausschließlich Feeds. Der Beitrag ist auf Ihrer Website sofort nach Veröffentlichung abrufbar und kann in den internen Sitemap-XML-Dateien aufgeführt werden.
3. Custom Post Types in den Haupt-Feed integrieren
Viele moderne WordPress-Installationen speichern Inhalte nicht als klassische Blog-Beiträge (post), sondern in Custom Post Types wie wissen, fallstudie, event oder release.
Standardmäßig lädt die Haupt-Feed-Abfrage (/feed/) nur den Beitragstyp post. Das führt dazu, dass spezialisierte Publikationen im Syndizierungsstrom unsichtbar bleiben.
Um mehrere Custom Post Types in den Feed einzubinden, greifen Entwickler in den Action-Hook pre_get_posts ein. Entscheidend ist hierbei die exakte Prüfung, dass die Abfrage tatsächlich eine Hauptabfrage (is_main_query()) im Feed-Modus (is_feed()) darstellt und nicht im Administrationsbereich oder in separaten Widgets ausgeführt wird.
/**
* Fügt Custom Post Types zur Hauptabfrage des Feeds hinzu.
*
* @param \WP_Query $query Das WP_Query-Objekt.
*/
function wppoland_include_cpt_in_feed( \WP_Query $query ): void {
if ( is_admin() || ! $query->is_main_query() || ! $query->is_feed() ) {
return;
}
$registered_types = $query->get( 'post_type' );
// Wenn kein spezieller Typ definiert ist oder nur der Standardtyp vorliegt
if ( empty( $registered_types ) || 'post' === $registered_types ) {
$allowed_types = array( 'post', 'case_study', 'tech_guide' );
$query->set( 'post_type', $allowed_types );
}
}
add_action( 'pre_get_posts', 'wppoland_include_cpt_in_feed' );Überprüfen Sie nach dieser Änderung, ob der RSS-Feed unter https://example.de/feed/ valide XML-Knoten für alle deklarierten Post Types liefert.
4. Eigene Podcast-Feeds ohne Third-Party-Plugins
Podcasting erfordert keine überdimensionierten All-in-One-Plugins, die Datenbanktabellen anlegen und eigene Tracking-Server zwischenschalten. WordPress verfügt im Kern über die Funktion add_feed(), mit der Sie beliebig viele spezialisierte Feeds (beispielsweise /feed/podcast/) erzeugen können.
Apple Podcasts und Spotify verlangen spezifische Tags im RSS-Header (itunes:author, itunes:category, itunes:explicit, itunes:image) sowie ein gültiges <enclosure>-Tag pro Audio-Datei mit exakter Bytegröße und MIME-Typ (audio/mpeg).
Schritt 1: Feed-Endpunkt registrieren
/**
* Registriert den benutzerdefinierten Feed-Endpunkt /feed/podcast/.
*/
function wppoland_register_podcast_feed(): void {
add_feed( 'podcast', 'wppoland_render_podcast_feed' );
}
add_action( 'init', 'wppoland_register_podcast_feed' );Nachdem Sie add_feed() hinzugefügt haben, müssen die Rewrite-Regeln einmalig neu eingelesen werden (beispielsweise durch Aufruf der Seite Einstellungen > Permalinks im Admin-Bereich oder programmatisch via flush_rewrite_rules(false) im Installations-Hook).
Schritt 2: Template für den Podcast-Feed ausgeben
/**
* Rendert das Podcast-spezifische RSS-XML.
*/
function wppoland_render_podcast_feed(): void {
header( 'Content-Type: application/rss+xml; charset=' . get_option( 'blog_charset' ), true );
$podcast_query = new \WP_Query( array(
'post_type' => 'podcast',
'post_status' => 'publish',
'posts_per_page' => 50,
) );
echo '<?xml version="1.0" encoding="' . esc_attr( get_option( 'blog_charset' ) ) . '"?>' . "\n";
?>
<rss version="2.0"
xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title><?php echo esc_html( get_bloginfo( 'name' ) ); ?> Podcast</title>
<link><?php echo esc_url( home_url( '/podcast/' ) ); ?></link>
<language><?php echo esc_html( get_bloginfo( 'language' ) ); ?></language>
<itunes:author><?php echo esc_html( get_bloginfo( 'name' ) ); ?></itunes:author>
<itunes:summary><?php echo esc_html( get_bloginfo( 'description' ) ); ?></itunes:summary>
<itunes:explicit>false</itunes:explicit>
<itunes:category text="Technology" />
<itunes:image href="<?php echo esc_url( home_url( '/images/podcast-cover.jpg' ) ); ?>" />
<atom:link href="<?php echo esc_url( home_url( '/feed/podcast/' ) ); ?>" rel="self" type="application/rss+xml" />
<?php while ( $podcast_query->have_posts() ) : $podcast_query->the_post();
$post_id = get_the_ID();
$audio_url = get_post_meta( $post_id, '_podcast_audio_url', true );
$filesize = absint( get_post_meta( $post_id, '_podcast_filesize', true ) ) ?: 25000000;
$duration = get_post_meta( $post_id, '_podcast_duration', true ) ?: '00:25:00';
?>
<item>
<title><?php echo esc_html( get_the_title() ); ?></title>
<link><?php echo esc_url( get_permalink() ); ?></link>
<pubDate><?php echo esc_html( mysql2date( 'D, d M Y H:i:s +0000', get_post_time( 'Y-m-d H:i:s', true ), false ) ); ?></pubDate>
<guid isPermaLink="false"><?php echo esc_html( 'podcast-' . $post_id ); ?></guid>
<description><![CDATA[<?php echo wp_strip_all_tags( get_the_excerpt() ); ?>]]></description>
<itunes:duration><?php echo esc_html( $duration ); ?></itunes:duration>
<itunes:explicit>false</itunes:explicit>
<?php if ( ! empty( $audio_url ) ) : ?>
<enclosure url="<?php echo esc_url( $audio_url ); ?>" length="<?php echo esc_attr( $filesize ); ?>" type="audio/mpeg" />
<?php endif; ?>
</item>
<?php endwhile; wp_reset_postdata(); ?>
</channel>
</rss>
<?php
exit;
}5. Moderne JSON Feeds implementieren (JSON Feed v1.1)
Während XML historisch gewachsen ist, bevorzugen Entwickler von headless Frontends (Astro, Next.js, Vue, mobile iOS- und Android-Clients) ein sauberes JSON-Format. Die Spezifikation JSON Feed 1.1 definiert ein leichtgewichtiges Datenformat, das ohne komplexe XML-Parser auskommt.
WordPress unterstützt JSON Feed nicht nativ, lässt sich jedoch über denselben add_feed()-Mechanismus um einen Endpunkt unter /feed/json/ ergänzen.
/**
* Registriert den Feed-Endpunkt /feed/json/.
*/
function wppoland_register_json_feed(): void {
add_feed( 'json', 'wppoland_render_json_feed' );
}
add_action( 'init', 'wppoland_register_json_feed' );
/**
* Erzeugt einen standardkonformen JSON Feed 1.1.
*/
function wppoland_render_json_feed(): void {
header( 'Content-Type: application/feed+json; charset=utf-8', true );
$posts_query = new \WP_Query( array(
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => absint( get_option( 'posts_per_rss', 15 ) ),
) );
$feed_data = array(
'version' => 'https://jsonfeed.org/version/1.1',
'title' => get_bloginfo( 'name' ),
'home_page_url' => home_url( '/' ),
'feed_url' => home_url( '/feed/json/' ),
'description' => get_bloginfo( 'description' ),
'language' => get_bloginfo( 'language' ),
'items' => array(),
);
while ( $posts_query->have_posts() ) {
$posts_query->the_post();
$post_id = get_the_ID();
$item = array(
'id' => (string) $post_id,
'url' => get_permalink(),
'title' => get_the_title(),
'content_html' => get_the_content_feed(),
'summary' => wp_strip_all_tags( get_the_excerpt() ),
'date_published' => get_post_time( 'c', true ),
'date_modified' => get_post_modified_time( 'c', true ),
);
if ( has_post_thumbnail( $post_id ) ) {
$image_url = get_the_post_thumbnail_url( $post_id, 'large' );
if ( $image_url ) {
$item['image'] = $image_url;
}
}
$feed_data['items'][] = $item;
}
wp_reset_postdata();
echo wp_json_encode( $feed_data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT );
exit;
}6. Server-Performance, Caching und HTTP-304-Validierung
Standard-Feeds in WordPress belasten den Webserver unverhältnismäßig stark. Jeder Aufruf von /feed/ triggert eine vollständige WordPress-Initialisierung, Datenbankabfragen zur Ermittlung der neuesten Beiträge und rechenintensive Textfilter wie wpautop oder Shortcode-Parser. Da RSS-Reader und Web-Crawler Feeds in engen Abständen abrufen, kann ungesicherter Feed-Traffic bei Traffic-Spitzen zu hoher CPU-Last führen.
HTTP Conditional Requests (Last-Modified und ETag)
Ein gut konfigurierter Feed-Server muss bedingte Anfragen (If-Modified-Since und If-None-Match) unterstützen. Wenn sich seit der letzten Abfrage kein neuer Beitrag geändert hat, sendet WordPress sofort einen HTTP-Statuscode 304 Not Modified zurück, ohne den gesamten XML-Body zu übertragen.
WordPress bringt die Helferfunktion get_lastpostmodified('GMT') mit. Prüfen Sie in benutzerdefinierten Feed-Callbacks stets den Zeitstempel des letzten Beitrags:
/**
* Sendet 304 Not Modified Header für RSS-Feeds, falls keine Änderungen vorliegen.
*/
function wppoland_validate_feed_cache_headers(): void {
if ( ! is_feed() ) {
return;
}
$last_modified_gmt = get_lastpostmodified( 'GMT' );
if ( ! $last_modified_gmt ) {
return;
}
$last_modified_timestamp = strtotime( $last_modified_gmt );
$etag = '"' . md5( $last_modified_gmt ) . '"';
header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $last_modified_timestamp ) . ' GMT' );
header( 'ETag: ' . $etag );
$client_if_modified_since = isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ? trim( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) : false;
$client_if_none_match = isset( $_SERVER['HTTP_IF_NONE_MATCH'] ) ? trim( $_SERVER['HTTP_IF_NONE_MATCH'] ) : false;
if ( ( $client_if_none_match && $client_if_none_match === $etag ) ||
( $client_if_modified_since && strtotime( $client_if_modified_since ) >= $last_modified_timestamp ) ) {
status_header( 304 );
exit;
}
}
add_action( 'template_redirect', 'wppoland_validate_feed_cache_headers', 1 );Edge- und Reverse-Proxy-Caching
Liegt Ihre Website hinter einem Reverse Proxy wie Nginx, Varnish oder einem CDN wie Cloudflare, sollten Feeds mit einem angemessenen Cache-Control-Header versehen werden (z. B. s-maxage=900, max-age=300).
Auf diese Weise beantwortet der CDN-Edge-Server 99 Prozent aller Aggregator-Anfragen direkt aus dem RAM, während der PHP-Prozess und die MySQL-Datenbank vollständig entlastet bleiben.
Mehrsprachige Feeds und hreflang-Fallen
Wenn Sie WPML, Polylang oder eigene Locale-Slugs nutzen, erzeugt WordPress oft mehrere Feed-URLs. Typische Fehler:
- Der Hauptfeed mischt Sprachversionen, weil
pre_get_postsdie Locale nicht filtert. - Ein CDN cached
/feed/ohneVaryauf Cookie oder Pfad, und liefert EN-Inhalte an DE-Abonnenten. - Enclosure-URLs zeigen auf die falsche Domain nach Domain-Mapping.
Halten Sie pro Sprache einen klaren Feed-Endpunkt, filtern Sie lang bzw. Taxonomien früh im Query, und testen Sie den Feed mit einem Reader, der wirklich HTTP-304 und ETag respektiert - nicht nur mit dem Browser-Tab.
Für Redaktionen mit mehreren Marken lohnt ein eigener add_feed( 'brand-a' )-Endpunkt statt Query-Parameter am Hauptfeed. Parameter-Feeds brechen Caching und teilen sich oft denselben ETag-Schlüssel. Zusätzlich lohnt ein kurzer Feed-Smoke-Test nach jedem Theme-Release.
Fazit und Best Practices
RSS ist kein Auslaufmodell, sondern eine performante und dezentrale Schnittstelle. Mit den richtigen Core-Hooks passen Sie den Ausgabestrom flexibel an:
- Bilder sauber deklarieren: Nutzen Sie sowohl Inline-HTML für Newsletter-Systeme als auch Enclosure- bzw. Media-Tags für standardkonforme Feed-Reader.
- Scraper-Verzögerung etablieren: Schützen Sie Ihre exklusiven Inhalte durch eine 30- bis 60-minütige Veröffentlichungsverzögerung im SQL-Filter.
- Endpunkte spezialisieren: Überlassen Sie schwere Audio- und JSON-Ausgaben dedizierten Endpunkten via
add_feed(), anstatt den Standardfeed zu überfrachten. - Caching konsequent durchsetzen: Verwenden Sie ETag-Validierung und CDN-Header, um unproduktive Serverlast zu vermeiden.
Wenn Sie hochfrequentierte Feeds, mehrsprachige Syndizierungsnetzwerke oder Headless-Schnittstellen in Ihre bestehende Architektur integrieren möchten, unterstützt Sie unser Team bei der Konzeption und Umsetzung. Erfahren Sie mehr über unsere Arbeit an modernen WordPress-Systemen.






