WordPress RSS-Feeds meistern: Leitfaden für Entwickler

WordPress RSS-Feeds meistern: Leitfaden für Entwickler

Zuletzt überprüft: 12. Oktober 2026
24 Min. Lesezeit
Leitfaden
Full-Stack-Entwickler

WordPress liefert Inhalte schon seit Jahren als XML aus, lange bevor jemand den Begriff “Headless CMS” erfunden hat. Der RSS-Feed ist nach wie vor der einfachste Vertrag: eine Liste von Einträgen, Titel, Inhalt oder Auszug, Datum, kanonischer Link. Aggregatoren, Newsletter-Importer, Feed-Reader und Podcast-Verzeichnisse verarbeiten ihn bis heute.

Dieser Text richtet sich an Entwickler, die ein Theme oder ein kleines Plugin betreuen und ihre Feeds im Griff haben wollen, ohne ein “RSS everything”-Paket zu installieren. Er behandelt die Standard-Endpunkte, das, was das RSS2-Template im Vergleich zur Spezifikation tatsächlich ausgibt, add_feed, Caching, sauberes Abschalten, die Enumeration von Benutzern über den Autoren-Feed, Podcasts, JSON Feed und WebSub. Die Aussagen zum Verhalten des Core sind am Quellcode von WordPress geprüft (wp-includes/feed-rss2.php, feed-atom.php, feed.php, class-wp.php).

#Welche Standard-RSS-Feeds WordPress bereitstellt

Der Core registriert mehrere Feed-Typen. Mit aktivierten sprechenden Permalinks sehen Sie meist:

  • /feed/ oder /feed/rss2/: RSS 2.0 (Standard)
  • /feed/atom/: Atom
  • /feed/rdf/: RDF/RSS 1.0
  • /comments/feed/: globale Kommentare
  • /category/slug/feed/, /tag/slug/feed/, /author/login/feed/, /search/begriff/feed/: kontextbezogene Feeds

Ohne sprechende Permalinks kommen dieselben Daten über den Query-String (?feed=rss2). Die Funktion do_feed() liest die Variable feed aus und löst die Aktion do_feed_{name} aus; die Standard-Handler laden die Dateien wp-includes/feed-*.php. Gibt es für einen Namen keine Aktion, beendet WordPress die Anfrage mit einem 404-Fehler.

Im Produktionsbetrieb zählen drei Dinge:

  1. Discovery: Der Core gibt <link rel="alternate" type="application/rss+xml" ...> in wp_head über die Funktionen feed_links und feed_links_extra aus, sofern das Theme add_theme_support( 'automatic-feed-links' ) deklariert. Reader und Crawler nehmen, was sie im HTML sehen, nicht das, was “da sein sollte”.
  2. Abfrageumfang: Der Haupt-Feed enthält Beiträge (post). CPTs, Seiten und eigene Status kommen erst hinein, wenn Sie pre_get_posts anpassen.
  3. Volltext oder Auszug: Die Option rss_use_excerpt (Einstellungen, Lesen) bestimmt die Form der Einträge. Das RSS2-Template gibt immer <description> mit dem Auszug aus (the_excerpt_rss()) und ergänzt bei Volltext <content:encoded> mit dem Inhalt aus get_the_content_feed(). Die Filter the_content_feed und the_excerpt_rss wirken auf genau diese beiden Felder.

Wenn Sie einen Newsletter-Importer bauen, prüfen Sie zuerst /feed/ ohne Plugins. Oft ist das Problem nicht “kein RSS”, sondern ein fehlendes Vorschaubild, fehlende CPTs oder ein zu aggressives CDN, das XML wie HTML cacht.

Hooks der XML-Ebene, die Sie kennen sollten, bevor Sie ein eigenes Template schreiben: rss2_ns (Namespaces im Wurzelelement), rss2_head (Kindelemente von <channel>), rss2_item (Kindelemente jedes <item>) sowie die Atom-Gegenstücke (atom_ns, atom_head, atom_entry). Die Inhaltsfilter (the_content_feed, the_excerpt_rss) greifen früher, auf den HTML-String, der in den Eintrag eingefügt wird. Die Reihenfolge zählt: Zuerst bauen Sie den Inhalt des Eintrags, danach ergänzen Sie die XML-Elemente drumherum.

Die Anzahl der Einträge im Haupt-Feed steuert die Option posts_per_rss (Einstellungen, Lesen): WP_Query setzt sie bei Feed-Abfragen als posts_per_page ein, und dasselbe Limit kürzt den Kommentar-Feed. Erhöhen Sie den Wert nicht “sicherheitshalber” auf Hunderte: Schweres XML mit Volltext und Shortcodes ruiniert die TTFB und sprengt auf Shared Hosting die PHP-Speichergrenzen.

#Was das RSS2-Template von WordPress laut RSS-2.0-Spezifikation ausgibt

Die RSS-2.0-Spezifikation verlangt im Kanal drei Elemente: title, link und description. Innerhalb eines Eintrags sind alle Elemente optional, mindestens eines von title oder description muss aber vorhanden sein. Datumsangaben folgen dem Format aus RFC 822. Das Template feed-rss2.php erfüllt dieses Minimum und ergänzt einige Elemente, die Sie kennen sollten, bevor Sie sie in Hooks doppelt ausgeben:

  • <atom:link rel="self"> mit der Adresse des Feeds selbst (der Atom-Namespace ist im RSS2-Wurzelelement deklariert),
  • <lastBuildDate> und <language> aus den Website-Einstellungen,
  • <sy:updatePeriod> und <sy:updateFrequency> aus dem Syndication-Modul, standardmäßig hourly und 1, änderbar über die Filter rss_update_period und rss_update_frequency,
  • in jedem Eintrag <dc:creator>, Kategorien, <pubDate> in der Zeitzone +0000 und <guid isPermaLink="false">.

Das Element <ttl> (Anzahl der Minuten, die der Kanal gecacht werden darf) fehlt im Core-Template. Wenn Ihr Reader oder Aggregator es berücksichtigt, ergänzen Sie es über rss2_head.

guid verdient einen eigenen Blick. Die Spezifikation beschreibt es als Zeichenkette, die einen Eintrag eindeutig identifiziert; isPermaLink ist standardmäßig true, und bei false darf der Reader nicht annehmen, dass es sich um eine URL handelt. WordPress gibt das Feld guid des Beitrags aus, das beim Anlegen festgelegt wird (meist in der Form https://domain/?p=123), und markiert es mit isPermaLink="false". Das ist die richtige Wahl: Eine Änderung von Slug oder Domain ändert den Bezeichner nicht, also zeigen Reader alte Beiträge nicht als neu an. Überschreiben Sie guid bei einer Domainmigration nicht, wenn Ihre Abonnenten nicht das komplette Archiv noch einmal bekommen sollen.

Die Spezifikation enthält außerdem <enclosure> mit drei Pflichtattributen: url, length (Größe in Bytes) und type (MIME-Typ). Das RSS2-Template ruft rss_enclosure() auf, das das benutzerdefinierte Feld enclosure des Beitrags liest (URL, Länge und Typ in aufeinanderfolgenden Zeilen) und das fertige Element ausgibt. Bevor Sie ein eigenes enclosure in rss2_item ergänzen, prüfen Sie, ob der Beitrag dieses Feld nicht schon hat, sonst entsteht ein Duplikat.

#Atom und RSS 2.0 in WordPress im Vergleich

Atom ist in RFC 4287 aus dem Jahr 2005 beschrieben und strenger als RSS. Das Element atom:feed muss genau ein atom:id, ein atom:title und ein atom:updated enthalten sowie mindestens ein atom:author, es sei denn, jeder Eintrag hat einen eigenen Autor. Dieselben drei Elemente sind in jedem atom:entry Pflicht. Der Feed sollte ein atom:link mit rel="self" haben, Datumsangaben müssen dem Format date-time aus RFC 3339 entsprechen, und der MIME-Typ ist application/atom+xml.

Das Template feed-atom.php setzt das so um: Die <id> des Kanals ist die Adresse des Atom-Feeds, die <id> eines Eintrags ist guid, jeder Eintrag hat ein <author>, und neben <published> steht <updated> mit dem Datum der letzten Änderung des Beitrags. Das ist ein praktischer Unterschied: Der RSS2-Feed des Core hat nur pubDate, eine Korrektur in einem veröffentlichten Text hinterlässt dort also keine Spur, während Atom sie über updated signalisiert. Wenn Ihre Abnehmer Inhaltsänderungen synchronisieren (Archive, interne Suchmaschinen, Importer), geben Sie ihnen /feed/atom/.

#Beitragsbilder und Medien zum RSS-Feed hinzufügen

Der Standard-Feed fügt das Beitragsbild nicht in den Inhalt ein. Die meisten Newsletter-Tools suchen ein <img> im Inhalt des Eintrags oder in Media RSS. Die minimale, vorhersehbare Korrektur:

declare(strict_types=1);

function wppoland_feed_prepend_thumbnail( string $content ): string {
	if ( ! is_feed() || ! has_post_thumbnail() ) {
		return $content;
	}

	$post_id = get_the_ID();
	$url     = get_the_post_thumbnail_url( $post_id, 'medium_large' );

	if ( ! $url ) {
		return $content;
	}

	$img = sprintf(
		'<p><img src="%s" alt="%s" /></p>',
		esc_url( $url ),
		esc_attr( get_the_title( $post_id ) )
	);

	return $img . $content;
}
add_filter( 'the_content_feed', 'wppoland_feed_prepend_thumbnail' );
add_filter( 'the_excerpt_rss', 'wppoland_feed_prepend_thumbnail' );

Bei Volltext im Feed landen beide Filter im selben Eintrag (Auszug in <description>, Inhalt in <content:encoded>), das Bild erscheint also in beiden Feldern. Die meisten Reader zeigen content:encoded, Newsletter-Importer lesen dagegen oft description, deshalb lohnt es sich, beide Filter zu behalten.

Für Reader, die Media RSS unterstützen, können Sie zusätzlich den Namespace xmlns:media="http://search.yahoo.com/mrss/" über rss2_ns und das Element media:content über rss2_item ergänzen:

declare(strict_types=1);

function wppoland_media_ns(): void {
	echo 'xmlns:media="http://search.yahoo.com/mrss/"' . "\n";
}
add_action( 'rss2_ns', 'wppoland_media_ns' );

function wppoland_media_content(): void {
	$thumb_id = get_post_thumbnail_id();
	$src      = $thumb_id ? wp_get_attachment_image_src( $thumb_id, 'large' ) : false;

	if ( ! $src ) {
		return;
	}

	printf(
		'<media:content url="%s" medium="image" type="%s" width="%d" height="%d" />' . "\n",
		esc_url( $src[0] ),
		esc_attr( (string) get_post_mime_type( $thumb_id ) ),
		(int) $src[1],
		(int) $src[2]
	);
}
add_action( 'rss2_item', 'wppoland_media_content' );

Mischen Sie beide Ansätze nicht ohne Grund: ein Kanal, eine Konvention, weniger Überraschungen in den Importern.

#Veröffentlichung im WordPress-Feed verzögern

Scraper und Aggregatoren holen /feed/ wenige Sekunden nach der Veröffentlichung ab. Wird das Original langsamer indexiert als die Kopie, haben Sie den klassischen Streit um die Kanonizität. Eine Verzögerung der Einträge im Feed-Query um eine Stunde verschafft dem Crawler der eigenen Domain einen zeitlichen Vorsprung:

declare(strict_types=1);

function wppoland_delay_feed_posts( string $where, WP_Query $query ): string {
	global $wpdb;

	if ( ! $query->is_main_query() || ! $query->is_feed() || $query->is_comment_feed() ) {
		return $where;
	}

	$where .= $wpdb->prepare(
		" AND {$wpdb->posts}.post_date_gmt <= %s ",
		gmdate( 'Y-m-d H:i:s', time() - HOUR_IN_SECONDS )
	);

	return $where;
}
add_filter( 'posts_where', 'wppoland_delay_feed_posts', 10, 2 );

Der Filter posts_where übergibt das Query-Objekt als zweites Argument, die Bedingung betrifft also nur die Hauptabfrage des Feeds und nicht Widgets oder Blöcke, die in derselben Anfrage gerendert werden. Der Vergleich der Spalte mit einem fertigen Datum (statt TIMESTAMPDIFF auf der Spalte) erlaubt der Datenbank, den Index auf post_date_gmt zu nutzen.

Das ersetzt weder rel=canonical noch die Sitemap. Es ist nur ein Zeitpuffer auf der XML-Ebene. Auf Websites mit häufigen Korrekturen nach der Veröffentlichung setzen Sie eine längere Schwelle oder schließen Entwurfsstatus auf anderem Weg aus.

#Custom Post Types im Haupt-Feed von WordPress

Das Haupt-/feed/ liest den Typ post. Portfolio, Veranstaltungen oder Release Notes bleiben außen vor, bis Sie die Abfrage erweitern:

declare(strict_types=1);

function wppoland_include_cpt_in_main_feed( WP_Query $query ): void {
	if ( is_admin() || ! $query->is_main_query() || ! $query->is_feed() ) {
		return;
	}

	// Nur der Haupt-Feed, nicht /author/.../feed/ und nicht Kommentare.
	if ( $query->is_comment_feed() || $query->is_author() || $query->is_category() || $query->is_tag() || $query->is_search() ) {
		return;
	}

	$query->set(
		'post_type',
		array( 'post', 'portfolio', 'event' )
	);
}
add_action( 'pre_get_posts', 'wppoland_include_cpt_in_main_feed' );

Die Bedingung is_main_query() ist entscheidend: Ohne sie ändern Sie jede Abfrage, die während der Feed-Erzeugung läuft, einschließlich der Abfragen von Plugins. Ein separater CPT-Feed (/feed/portfolio/) ist oft sauberer, als Typen in einem XML zu mischen. Ein Inhaltstyp, der mit has_archive registriert ist, hat außerdem einen eigenen Feed unter der Archivadresse, zum Beispiel /portfolio/feed/.

#Eigenen RSS-Feed mit add_feed erstellen

add_feed( $feedname, $callback ) registriert die Rewrite-Regel /feed/{name}/ und ruft den Callback anstelle des Standard-RSS2-Templates auf. Das ist die offizielle API aus der Feeds-Dokumentation und der Referenz zu add_feed. Der Callback erhält zwei Argumente: die Angabe, ob es sich um einen Kommentar-Feed handelt, und den Namen des Feeds.

Ein Muster für die Produktion:

declare(strict_types=1);

function wppoland_register_release_feed(): void {
	add_feed(
		'releases',
		static function (): void {
			header( 'Content-Type: application/rss+xml; charset=' . get_option( 'blog_charset' ), true );
			echo '<?xml version="1.0" encoding="' . esc_attr( get_option( 'blog_charset' ) ) . '"?' . '>';
			// Hier bauen Sie das vollständige RSS2-Dokument oder laden ein eigenes Template.
			load_template( get_stylesheet_directory() . '/feed-releases.php' );
		}
	);
}
add_action( 'init', 'wppoland_register_release_feed' );

Nach der ersten Registrierung (oder einer Umbenennung des Feeds) führen Sie flush_rewrite_rules() einmal aus, bei der Aktivierung des Plugins, nicht bei jedem init. Ohne Flush bekommen Sie unter /feed/releases/ einen 404, obwohl der Callback-Code korrekt ist.

Im Callback:

  • setzen Sie Content-Type, bevor Sie das erste Byte XML ausgeben,
  • escapen Sie jedes benutzerdefinierte Feld (esc_xml / esc_url / esc_html),
  • begrenzen Sie die Abfrage (posts_per_page, Status publish, gegebenenfalls eine Meta-Query),
  • rufen Sie keine schweren Shortcodes ohne Cache auf.

Der Header im Callback ist aus einem konkreten Grund nötig. Bei jeder Feed-Anfrage setzt WP::send_headers() den Content-Type aus der Funktion feed_content_type(), die nur rss, rss2, rss-http, atom und rdf kennt. Für einen unbekannten Namen liefert sie application/octet-stream. Entweder überschreiben Sie den Header im Callback (zweites Argument true), oder Sie ergänzen den Typ über den Filter feed_content_type, der den MIME-Typ und den Namen des Feeds erhält.

Wenn Sie nur eine Variante des Haupt-RSS2 brauchen (andere Anzahl von Einträgen, anderer Typ), reicht manchmal add_feed plus pre_get_posts auf is_feed( 'releases' ) und das Standard-Template. Eine eigene Template-Datei lohnt sich, wenn das XML andere Namespaces oder eine andere Struktur der Einträge hat.

Der Feed-Name landet im Rewrite-Tag feed. Vorsicht bei Namen aus dem Core (rss2, atom, rdf, rss): add_feed entfernt den Standard-Handler do_feed_{name} und hängt Ihren Callback ein, add_feed( 'rss2', ... ) ersetzt also stillschweigend den Haupt-Feed der gesamten Website. Nach einer Umbenennung funktioniert die alte URL nicht mehr, bis Sie eine Weiterleitung in der Serverkonfiguration oder in template_redirect ergänzen. Bei lokalen Tests öffnen Sie nach add_feed Einstellungen, Permalinks und speichern ohne Änderungen, falls Sie keinen Flush in der Plugin-Aktivierung haben. Das ist derselbe Mechanismus.

#Wie das Feed-Caching in WordPress funktioniert

Hier gibt es drei verschiedene Mechanismen, die leicht verwechselt werden: den Cache für externe Feeds, die WordPress abruft, bedingte Antworten für Ihre eigenen Feeds und den Cache im CDN.

Externe Feeds. fetch_feed() baut aus einer RSS- oder Atom-Adresse ein SimplePie-Objekt und gibt es zurück, im Fehlerfall ein WP_Error. Die Funktion akzeptiert auch ein Array von Adressen und führt sie zu einem Kanal zusammen. Das Ergebnis landet in Transients, die Lebensdauer ändert der Filter wp_feed_cache_transient_lifetime (Standard 12 Stunden). Das ist nicht der Cache Ihres /feed/.

declare(strict_types=1);

$rss = fetch_feed( 'https://example.com/feed/' );

if ( ! is_wp_error( $rss ) ) {
	$max   = $rss->get_item_quantity( 5 );
	$items = $rss->get_items( 0, $max );

	foreach ( $items as $item ) {
		printf(
			'<li><a href="%s">%s</a></li>',
			esc_url( $item->get_permalink() ),
			esc_html( $item->get_title() )
		);
	}
}

add_filter(
	'wp_feed_cache_transient_lifetime',
	static function (): int {
		return HOUR_IN_SECONDS;
	}
);

Die Prüfung mit is_wp_error() ist keine Formalität: Ein nicht erreichbarer Quellserver oder kaputtes XML sind Alltag, und ohne diese Bedingung wirft das Widget im Frontend einen Fatal Error. Inhalte aus SimplePie laufen durch einen Sanitizer auf Basis von KSES, Titel und Links escapen Sie bei der Ausgabe trotzdem.

Ihre Feeds und 304-Antworten. Der Core verarbeitet bedingte Anfragen für Feeds ohne jedes Plugin. WP::send_headers() berechnet bei jeder Anfrage mit gesetzter Variable feed den Wert Last-Modified aus dem Datum der letzten Änderung eines Beitrags (bei Kommentar-Feeds auch eines Kommentars), erzeugt daraus ein ETag und antwortet mit 304 und beendet die Anfrage, wenn der Client ein passendes If-Modified-Since oder If-None-Match schickt. Das gilt auch für Endpunkte aus add_feed, weil sie über dieselbe Variable laufen. Eigene 304-Logik schreiben Sie nur für Endpunkte außerhalb dieses Mechanismus, etwa in der REST API.

Eigener Endpunkt /feed/ oder /feed/releases/ am Edge:

  • CDN / Reverse Proxy: Setzen Sie Cache-Control bewusst. Für einen öffentlichen Feed reicht max-age=300 meist aus. Nach einer Veröffentlichung invalidieren Sie den Pfad /feed/ oder versionieren das ETag.
  • Object Cache: Wenn Sie schweres JSON erzeugen oder CPTs mit Meta-Daten verknüpfen, cachen Sie den Antwort-String per wp_cache_set mit kurzer TTL und leeren ihn bei transition_post_status / save_post.
  • Kein HTML pro Benutzer cachen: Der Feed ist öffentlich; Personalisierung im XML endet mit einem Datenleck zwischen Abonnenten.
  • Page Cache von Plugins: Manche Vollseiten-Caches liefern /feed/ als Seite mit falschem Content-Type aus. Schließen Sie feed-Pfade aus den Cache-Regeln aus oder erzwingen Sie einen Bypass für is_feed().

Ein Beispiel für einen kurzen Cache-Header bei einem eigenen Feed:

header( 'Cache-Control: public, max-age=300' );
header( 'X-Robots-Tag: noindex' ); // optional, wenn das XML nicht indexiert werden soll

noindex auf dem Feed schaltet die Syndikation nicht ab. Es verhindert nur, dass das XML selbst als “Seite” indexiert wird. Bei Cloudflare oder einem anderen Edge: eine Cache-Regel für *.xml / Pfad /feed* mit kurzer TTL und Cache Everything nur dann, wenn der Origin den korrekten MIME-Typ liefert. Ein falsch konfigurierter Edge-Cache kann nach einem Rewrite-Flush stundenlang ein leeres Dokument festhalten.

#RSS-Feeds in WordPress sicher deaktivieren oder einschränken

Feeds “hart” und ohne Plan abzuschalten hinterlässt verwaiste Discovery-Links und alte Lesezeichen in Feed-Readern. Die sichere Reihenfolge:

  1. Entfernen Sie die Discovery-Links: remove_action( 'wp_head', 'feed_links', 2 ) und remove_action( 'wp_head', 'feed_links_extra', 3 ), mit den Prioritäten, mit denen der Core sie registriert.
  2. Entscheiden Sie, ob alte URLs per 301 auf HTML-Inhalte weiterleiten, ein leeres 410 liefern oder weiterhin XML ausgeben.
  3. Erst danach schalten Sie die do_feed_*-Handler ab.

Der Core registriert vier Handler: do_feed_rdf, do_feed_rss, do_feed_rss2 und do_feed_atom. Eigene Handler für Kommentare gibt es nicht: /comments/feed/ läuft über do_feed_rss2 (oder do_feed_atom), wobei das erste Argument auf true gesetzt ist. Die minimale Abschaltung der klassischen Feeds (eigene add_feed-Feeds bleiben möglich):

declare(strict_types=1);

function wppoland_disable_default_feeds(): void {
	remove_action( 'do_feed_rdf', 'do_feed_rdf', 10 );
	remove_action( 'do_feed_rss', 'do_feed_rss', 10 );
	remove_action( 'do_feed_rss2', 'do_feed_rss2', 10 );
	remove_action( 'do_feed_atom', 'do_feed_atom', 10 );
}
add_action( 'init', 'wppoland_disable_default_feeds' );

function wppoland_feed_gone(): void {
	wp_die(
		esc_html__( 'Dieser Feed wurde deaktiviert.', 'wppoland' ),
		'',
		array( 'response' => 410 )
	);
}
add_action( 'do_feed_rss2', 'wppoland_feed_gone', 1 );
add_action( 'do_feed_atom', 'wppoland_feed_gone', 1 );

RDF und RSS 0.92 enden ohne Handler mit einem 404 aus do_feed(), RSS2 und Atom (samt ihren Kommentar-Varianten) bekommen 410.

Die sanftere Alternative: RSS2 behalten, Atom/RDF und den Kommentar-Feed entfernen. Aggregatoren nehmen ohnehin RSS2. Den Kommentar-Feed allein schalten Sie ab, indem Sie das Argument des Handlers prüfen:

declare(strict_types=1);

function wppoland_comments_feed_gone( bool $is_comment_feed ): void {
	if ( $is_comment_feed ) {
		wp_die( esc_html__( 'Der Kommentar-Feed ist deaktiviert.', 'wppoland' ), '', array( 'response' => 410 ) );
	}
}
add_action( 'do_feed_rss2', 'wppoland_comments_feed_gone', 1 );
add_action( 'do_feed_atom', 'wppoland_comments_feed_gone', 1 );
add_filter( 'feed_links_show_comments_feed', '__return_false' );

Der Filter feed_links_show_comments_feed entfernt den Discovery-Link zum Kommentar-Feed aus dem <head>. Entfernen Sie Feed-Rewrites nicht ohne Flush, sonst gibt es einen 404 statt einer kontrollierten Antwort.

#Wie der Autoren-Feed WordPress-Benutzer preisgibt

/author/admin/feed/ und /author/mariusz/feed/ bestätigen, dass ein Login existiert. Zusammen mit /?author=1 (Weiterleitung auf das Archiv) ist das der klassische Vektor zur Enumeration von Konten vor einem Brute-Force-Angriff oder gezieltem Phishing.

Harte Schließung der Autorenarchive (einschließlich der Feeds):

declare(strict_types=1);

function wppoland_disable_author_archives(): void {
	if ( is_author() ) {
		global $wp_query;
		$wp_query->set_404();
		status_header( 404 );
		nocache_headers();
	}
}
add_action( 'template_redirect', 'wppoland_disable_author_archives' );

Wenn die Autorenarchive bleiben sollen (persönlicher Blog), dann zumindest:

  • verwenden Sie einen sauberen nicename statt des rohen Logins in der URL, sofern Ihr Modell das zulässt,
  • veröffentlichen Sie keine Autorenliste mit Logins im HTML,
  • erwägen Sie, den Autoren-Feed auf eine leere Liste zu beschränken oder nur für is_feed() && is_author() einen 404 zu liefern.

Beachten Sie, dass das RSS2-Template in jedem Eintrag <dc:creator> mit dem Anzeigenamen des Autors ausgibt und Atom das Element <author>. Ist der Anzeigename identisch mit dem Login, gibt der Haupt-Feed ihn genauso preis wie das Autorenarchiv.

Die REST API (/wp-json/wp/v2/users) ist ein eigenes Thema. Das Abschalten des Autoren-Feeds schließt die Enumeration über REST ohne zusätzliche Filter nicht. Betrachten Sie RSS als eine von vielen Flächen, über die Identitäten sichtbar werden.

Legen Sie keine E-Mail-Adressen, Tokens, internen Bestell-IDs oder Inhalte mit Status private / draft in den Feed. Inhaltsfilter greifen auch im Feed-Kontext: Ein Shortcode, der im HTML einem Kunden ein Panel anzeigt, kann im XML an öffentliche Abonnenten durchsickern.

Der Kommentar-Feed (/comments/feed/ oder der Feed unter einem einzelnen Beitrag) wird in Audits gern übersehen. Wenn Kommentare Login-Daten, Bestellnummern oder personenbezogene Daten von B2B-Kunden enthalten, schalten Sie den Kommentar-Feed mit dem Code aus dem vorigen Abschnitt ab oder moderieren Sie, bevor ein Kommentar ins XML gelangt. Ein öffentlicher Kommentar-Feed ist eine eigene Angriffsfläche für Scraping, kein bloßer “Bonus für Leser”.

#Podcast-Feed in WordPress mit iTunes und enclosure erstellen

WordPress ist kein Host für Audiodateien, kann aber einen RSS-Feed erzeugen, den die Verzeichnisse akzeptieren. Die Anforderungen von Apple Podcasts sind eindeutig: Jede Folge braucht ein eindeutiges <enclosure> mit drei Bestandteilen (URL, Länge, Typ), jede Folge braucht eine GUID, die sich nie ändert, und der Feed muss mindestens eine Folge enthalten. Die Liste der Pflicht-Tags auf Kanalebene führt Apple in einem separaten Dokument zu RSS-Tags, und mit diesem gleichen Sie den Feed vor der Einreichung ab.

Technisch brauchen Sie:

  • den Namespace xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" (historische Namespace-URL; die Verzeichnisse erkennen sie),
  • Kanal-Tags aus dem Namensraum itunes (zum Beispiel itunes:author, itunes:category, itunes:image) sowie language,
  • Einträge mit <enclosure url="..." length="..." type="audio/mpeg" /> und optional itunes:duration.

Hooks aus dem Core:

declare(strict_types=1);

function wppoland_podcast_ns(): void {
	echo ' xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"';
}
add_action( 'rss2_ns', 'wppoland_podcast_ns' );

function wppoland_podcast_head(): void {
	echo '<itunes:author>' . esc_xml( get_bloginfo( 'name' ) ) . '</itunes:author>';
}
add_action( 'rss2_head', 'wppoland_podcast_head' );

function wppoland_podcast_item_enclosure(): void {
	$url = get_post_meta( get_the_ID(), 'episode_audio_url', true );
	$len = (int) get_post_meta( get_the_ID(), 'episode_audio_length', true );

	if ( ! is_string( $url ) || $url === '' || $len < 1 ) {
		return;
	}

	printf(
		'<enclosure url="%s" length="%d" type="audio/mpeg" />',
		esc_url( $url ),
		$len
	);
}
add_action( 'rss2_item', 'wppoland_podcast_item_enclosure' );

Dieser Code setzt voraus, dass die Folgen kein ausgefülltes Feld enclosure haben, das rss_enclosure() automatisch ausgibt. Die Funktion esc_xml() gibt es im Core seit Version 5.5, und sie ist das passende Escaping für Text in XML-Elementen.

Für die Produktion ist ein separater Feed (add_feed( 'podcast', ... )) meist besser, als normale Beiträge mit Folgen zu mischen. Verzeichnisse validieren den gesamten Kanal; ein fehlerhafter Eintrag kann die Einreichung scheitern lassen. Legen Sie MP3-Dateien unter einer stabilen URL ab (CDN), mit korrekter length in Bytes, denn geschätzte Längen bringen Clients durcheinander. Apple empfiehlt außerdem, auf dem Dateiserver HEAD-Anfragen und Anfragen mit Byte-Bereichen zu erlauben, von denen das Streaming abhängt.

#JSON Feed in WordPress hinzufügen

JSON Feed ist eine Alternative zu XML für JavaScript-Clients. Der WordPress-Core registriert ihn nicht nativ. Sie ergänzen ihn über add_feed( 'json', $callback ) und den Header Content-Type: application/feed+json, weil die Spezifikation diesen MIME-Typ empfiehlt.

Gerüst des Callbacks:

declare(strict_types=1);

function wppoland_json_feed(): void {
	header( 'Content-Type: application/feed+json; charset=utf-8', true );

	$query = new WP_Query(
		array(
			'post_type'           => 'post',
			'post_status'         => 'publish',
			'posts_per_page'      => 20,
			'ignore_sticky_posts' => true,
			'no_found_rows'       => true,
		)
	);

	$items = array();
	foreach ( $query->posts as $post ) {
		$items[] = array(
			'id'             => (string) get_the_guid( $post ),
			'url'            => get_permalink( $post ),
			'title'          => get_the_title( $post ),
			'content_html'   => apply_filters( 'the_content', $post->post_content ),
			'date_published' => get_post_time( DATE_ATOM, true, $post ),
			'date_modified'  => get_post_modified_time( DATE_ATOM, true, $post ),
		);
	}

	echo wp_json_encode(
		array(
			'version'       => 'https://jsonfeed.org/version/1.1',
			'title'         => get_bloginfo( 'name' ),
			'home_page_url' => home_url( '/' ),
			'feed_url'      => home_url( '/feed/json/' ),
			'language'      => get_bloginfo( 'language' ),
			'items'         => $items,
		),
		JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
	);
}

add_action(
	'init',
	static function (): void {
		add_feed( 'json', 'wppoland_json_feed' );
	}
);

JSON Feed ersetzt RSS weder bei Podcasts noch in älteren Readern. Behandeln Sie ihn als zweiten Vertrag für eigene Clients und interne Werkzeuge. Discovery im HTML: type="application/feed+json".

Die Spezifikation verlangt auf Kanalebene die Felder version, title und items und in jedem Eintrag id (String) sowie mindestens eines von content_html oder content_text. Version 1.1 hat das Feld authors eingeführt (ein Array, mehrere Autoren pro Kanal oder Eintrag) sowie das Feld language und das einzelne author als veraltet markiert. Halten Sie die id eines Eintrags stabil: Laut Spezifikation sollte sich die id nach einer Aktualisierung des Eintrags nicht ändern, und neue Einträge sollten keine bereits verwendete id nutzen. Deshalb ist die id im Beispiel die guid und nicht der Permalink, der sich mit dem Slug ändert. content_text oder content_html: Wählen Sie eines davon als maßgebliche Quelle; wer beide mit auseinanderlaufendem Inhalt liefert, bekommt in den Clients eine uneinheitliche Vorschau.

#WebSub für den WordPress-Feed aktivieren

Ein Reader ohne WebSub fragt den Feed in seinem eigenen Rhythmus ab, ein neuer Beitrag erreicht die Abonnenten also mit einer Verzögerung, die vom Reader abhängt. WebSub, eine W3C-Empfehlung, kehrt die Richtung um: Der Herausgeber nennt einen Hub, Abonnenten registrieren sich beim Hub, und der Hub verteilt die Aktualisierung, sobald der Herausgeber eine Änderung meldet.

Die Anforderungen auf Seiten des Herausgebers sind kurz. Die Spezifikation empfiehlt einen Link-Header mit mindestens einem rel="hub" und genau einem rel="self", bei XML-Feeds eingebettete link-Elemente. Der Herausgeber muss die genannten Hubs über die Aktualisierung des Themas informieren, wobei die Spezifikation den Benachrichtigungsmechanismus selbst offenlässt. In einer Anmerkung nennt sie ein Beispiel aus öffentlichen Hubs: eine POST-Anfrage mit den Schlüsseln hub.mode=publish und hub.url, das auf die geänderte Ressource zeigt.

Der WordPress-Core implementiert WebSub nicht. Im Plugin-Verzeichnis gibt es das Plugin “WebSub (FKA. PubSubHubbub)”, das beide Elemente ergänzt. Von Hand sieht das so aus:

declare(strict_types=1);

const WPPOLAND_HUB = 'https://hub.example.com/';

function wppoland_websub_links(): void {
	printf( '<atom:link rel="hub" href="%s" />' . "\n", esc_url( WPPOLAND_HUB ) );
}
add_action( 'rss2_head', 'wppoland_websub_links' );

function wppoland_websub_ping( string $new_status, string $old_status, WP_Post $post ): void {
	if ( 'publish' !== $new_status || 'publish' === $old_status || 'post' !== $post->post_type ) {
		return;
	}

	wp_remote_post(
		WPPOLAND_HUB,
		array(
			'body'     => array(
				'hub.mode' => 'publish',
				'hub.url'  => get_feed_link( 'rss2' ),
			),
			'blocking' => false,
		)
	);
}
add_action( 'transition_post_status', 'wppoland_websub_ping', 10, 3 );

rel="self" steht bereits im RSS2-Template (<atom:link rel="self">), Sie ergänzen also nur rel="hub". Die Adresse des Hubs und das Format der Benachrichtigung prüfen Sie in der Dokumentation des gewählten Hubs, weil die Spezifikation beides nicht vorschreibt. Die Bestätigung des Abonnements (hub.challenge), den Ablauf nach hub.lease_seconds und die Signatur X-Hub-Signature bei einem Secret übernehmen Hub und Abonnent, nicht WordPress.

#XML-Parsing-Fehler im WordPress-Feed beheben

Der häufigste Fehler bei eigenen Templates und Plugins ist ein Dokument, das der Reader mit einer Meldung wie XML or text declaration not at start of entity ablehnt. Die Deklaration <?xml ... ?> muss das erste Zeichen der Antwort sein. Schon eine Leerzeile oder ein davor ausgegebenes BOM-Zeichen genügt, und der Parser verweigert die Verarbeitung des gesamten Kanals.

Die Ursache liegt fast immer in einer PHP-Datei, die bei jeder Anfrage geladen wird: ein Leerzeichen oder Zeilenumbruch nach dem schließenden ?> in functions.php oder in einem Plugin, oder eine Datei, die als UTF-8 mit BOM gespeichert ist. Die richtige Korrektur besteht darin, das schließende ?> aus reinen PHP-Dateien zu entfernen und Dateien ohne BOM zu speichern. Um den Verursacher zu finden, führen Sie curl -s https://domain/feed/ | head -c 100 | xxd aus, sehen nach, was vor <?xml steht, und deaktivieren dann ein Plugin nach dem anderen.

Wenn Sie das Plugin eines Dritten nicht sofort korrigieren können, hilft vorübergehend ein Ausgabepuffer, der den Anfang der Feed-Antwort bereinigt:

declare(strict_types=1);

function wppoland_clean_feed_output(): void {
	if ( ! is_feed() ) {
		return;
	}

	ob_start(
		static function ( string $buffer ): string {
			// BOM und Leerraum vor der XML-Deklaration entfernen.
			return ltrim( preg_replace( '/^\xEF\xBB\xBF/', '', $buffer ) );
		}
	);
}
add_action( 'template_redirect', 'wppoland_clean_feed_output', 1 );

Das ist ein Flicken, keine Reparatur: Der Puffer entfernt keine Zeichen, die ausgegeben wurden, bevor template_redirect läuft. Teams mit Integrationstests können das automatisch überwachen. Die Feed-Tests im Core laden das Template direkt und unterdrücken Header-Warnungen; dasselbe Muster funktioniert in WP_UnitTestCase:

class Feed_Rss2_Test extends WP_UnitTestCase {
	public function test_rss2_is_well_formed(): void {
		self::factory()->post->create_many( 3 );
		$this->go_to( '/?feed=rss2' );

		ob_start();
		@require ABSPATH . WPINC . '/feed-rss2.php';
		$xml = ob_get_clean();

		$this->assertStringStartsWith( '<?xml', $xml );

		$dom = new DOMDocument();
		$this->assertTrue( $dom->loadXML( $xml ) );
	}
}

Die Assertion prüft die rohe Ausgabe ohne trim(), weil gerade Leerraum am Anfang hier der Fehler ist.

#RSS-Feeds in einem mehrsprachigen WordPress verwalten

Mit WPML, Polylang oder eigenen Sprachpräfixen stellt WordPress meist mehrere Feed-Adressen bereit. Typische Fehler:

  • der Haupt-Feed mischt Sprachversionen, weil pre_get_posts nicht nach Sprache filtert,
  • das CDN cacht /feed/ ohne Unterscheidung nach Pfad oder Cookie und liefert Inhalte einer Sprache an Abonnenten einer anderen,
  • Adressen in enclosure zeigen nach dem Domain-Mapping auf die falsche Domain,
  • das Element <language> im Kanal stammt aus den Website-Einstellungen und nicht aus der Sprache der Inhalte des jeweiligen Feeds.

Halten Sie einen klaren Endpunkt pro Sprache, filtern Sie Sprache oder Taxonomie früh in der Abfrage und testen Sie den Feed mit einem Reader, der tatsächlich If-None-Match sendet, nicht nur in einem Browser-Tab. Bei mehreren Marken in einer Installation ist ein eigenes add_feed( 'marke-a', ... ) sauberer als ein Query-Parameter, der an den Haupt-Feed angehängt wird: Parameter zersplittern den Cache im CDN, und das ETag des Core hängt ausschließlich vom Datum der letzten Änderung ab, nicht von Parametern.

#Was Sie vor dem Livegang eines eigenen RSS-Feeds prüfen sollten

Bevor Sie Änderungen in die Produktion bringen:

  1. Öffnen Sie /feed/ im Browser und mit curl -I: Status 200, sinnvoller Content-Type.
  2. Aktivieren Sie nach add_feed das Plugin bzw. führen Sie einen Rewrite-Flush aus und prüfen Sie die neue URL.
  3. Entfernen Sie überflüssige rel=alternate-Links, wenn Sie Feeds abschalten.
  4. Prüfen Sie /author/.../feed/ auf Enumeration.
  5. Bei einem Podcast schicken Sie das XML vor der Einreichung durch den Validator des Verzeichnisses.
  6. Bestätigen Sie nach der Veröffentlichung eines Testbeitrags, dass das CDN innerhalb einer akzeptablen TTL frisches XML liefert.
  7. Stellen Sie sicher, dass Shortcodes und dynamische Blöcke im Feed-Inhalt kein HTML des Admin-Panels und keine Sitzungsdaten ausgeben.
  8. Senden Sie eine zweite Anfrage mit dem Header If-None-Match, gesetzt auf das erhaltene ETag, und prüfen Sie, ob 304 zurückkommt.

RSS ist in WordPress eine API-Schicht, kein Relikt. Sie steuern Abfrage, Template, Cache und Angriffsfläche genauso wie bei einem REST-Endpunkt. Der Unterschied: Feed-Clients sind älter, verzeihen fehlerhaftes XML weniger und cachen Antworten eher auf ihrer eigenen Seite.

Offizielle Referenzen: Feeds (Developer Resources), add_feed sowie WordPress Feeds in der Administrationsdokumentation. Bevor Sie das nächste “RSS Pack”-Plugin schreiben, prüfen Sie, welcher dieser drei Verträge in Ihrem Theme tatsächlich fehlt.

Wenn Sie den Feed um eigene Inhaltstypen, Meta-Felder oder einen separaten Podcast-Kanal im Rahmen der WordPress-Entwicklung erweitern müssen, beginnen Sie mit einem add_feed und einem Template, ohne monolithisches “All Feeds”-Plugin.

Nächster Schritt

Machen Sie aus dem Artikel eine echte Umsetzung

Dieser Block stärkt die interne Verlinkung und führt Nutzer gezielt zum nächsten sinnvollen Schritt im Service- und Content-System.

Soll das Thema auf Ihrer Website umgesetzt werden?

Wenn Sie aus dem Artikel konkrete Maßnahmen für Website, Relaunch oder Weiterentwicklung ableiten wollen, definiere ich den Scope und setze ihn um.

Relevanter Cluster

Weitere WordPress-Dienste und Wissensbasis entdecken

Stärken Sie Ihr Unternehmen mit professionellem technischen Support in den Kernbereichen des WordPress-Ökosystems.

Wie binde ich Beitragsbilder sauber in den WordPress RSS-Feed ein?#
Über die Filter the_content_feed und the_excerpt_rss wird ein HTML-Bildtag vor den Text gestellt, sodass das Bild in description und in content:encoded erscheint. Für Reader, die Media RSS auswerten, wird der Namespace über rss2_ns deklariert und über den Hook rss2_item ein media:content-Element mit URL, MIME-Typ, Breite und Höhe ausgegeben.
Warum sollten RSS-Feeds für neue Beiträge zeitlich verzögert werden?#
Scraper und Aggregatoren holen /feed/ Sekunden nach der Veröffentlichung ab. Wird das Original langsamer indexiert als die Kopie, entsteht ein Streit um die Kanonizität. Eine Verzögerung von einer Stunde im Feed-Query über posts_where verschafft dem Crawler der eigenen Domain einen zeitlichen Vorsprung. Sie ersetzt weder rel=canonical noch die Sitemap.
Wie integriere ich Custom Post Types in den Standard-Feed?#
Über den Hook pre_get_posts prüfen Sie, ob eine Hauptabfrage für einen Feed vorliegt (is_main_query und is_feed), schließen Kommentar-, Autoren-, Kategorie-, Schlagwort- und Such-Feeds aus und übergeben dem Query-Objekt mit set('post_type', [...]) die gewünschten Inhaltstypen.
Welche Vorteile bietet JSON Feed gegenüber RSS 2.0 XML?#
JSON Feed ist eine Alternative zu XML für JavaScript-Clients und eigene interne Werkzeuge. Version 1.1 definiert unter anderem die Felder authors und language. WordPress registriert JSON Feed nicht nativ; Sie ergänzen ihn per add_feed und senden Content-Type application/feed+json. Für Podcasts und ältere Reader ersetzt er RSS nicht.
Wie verhindere ich, dass aggressive RSS-Reader und Web-Crawler den Server überlasten?#
Der Core sendet für Feeds bereits Last-Modified und ETag und beantwortet passende bedingte Anfragen mit 304 (WP::send_headers), auch für Endpunkte aus add_feed. Ergänzend setzen Sie bewusst Cache-Control (etwa max-age=300) für CDN oder Reverse Proxy, cachen teure Antworten im Object Cache mit kurzer TTL und halten posts_per_rss niedrig.

Sie brauchen ein FAQ für Branche und Zielmarkt? Wir erstellen eine Version passend zu Ihren Business-Zielen.

Kontakt aufnehmen

Ähnliche Artikel