Mistrzowskie kanały RSS w WordPressie: przewodnik dewelopera

Mistrzowskie kanały RSS w WordPressie: przewodnik dewelopera

Ostatnio zweryfikowano: 12 października 2026
21 min czytania
Przewodnik
Full-stack developer

WordPress od lat serwuje treść przez XML, zanim ktoś wymyślił “headless CMS”. Kanał RSS to nadal najprostszy kontrakt: lista pozycji, tytuł, treść albo skrót, data, link kanoniczny. Agregatory, newsletterowe importery, czytniki i katalogi podcastów nadal go zjadają.

Ten tekst jest dla dewelopera, który ma motyw albo małą wtyczkę i chce panować nad feedami bez pakietu “RSS everything”. Pokrywa domyślne endpointy, to, co szablon RSS2 faktycznie wysyła względem specyfikacji, add_feed, cache, bezpieczne wyłączanie, enumerację użytkowników przez feed autora, podcasty, JSON Feed i WebSub. Fragmenty o zachowaniu core są sprawdzone w kodzie źródłowym WordPressa (wp-includes/feed-rss2.php, feed-atom.php, feed.php, class-wp.php).

#Jakie domyślne kanały RSS ma WordPress

Core rejestruje kilka typów feedów. Po włączeniu ładnych permalinków najczęściej widzisz:

  • /feed/ albo /feed/rss2/: RSS 2.0 (domyślny)
  • /feed/atom/: Atom
  • /feed/rdf/: RDF/RSS 1.0
  • /comments/feed/: komentarze globalne
  • /category/slug/feed/, /tag/slug/feed/, /author/login/feed/, /search/fraza/feed/: feedy kontekstowe

Bez ładnych permalinków te same dane wychodzą przez query string (?feed=rss2). Funkcja do_feed() odczytuje zmienną feed i odpala akcję do_feed_{nazwa}; domyślne handlery ładują pliki wp-includes/feed-*.php. Jeśli dla danej nazwy nie ma żadnej akcji, WordPress kończy żądanie błędem 404.

W praktyce produkcyjnej trzy rzeczy są ważne:

  1. Discovery: core wypisuje <link rel="alternate" type="application/rss+xml" ...> w wp_head przez funkcje feed_links i feed_links_extra, o ile motyw deklaruje add_theme_support( 'automatic-feed-links' ). Czytnik i crawler biorą to, co widzą w HTML, nie to, co “powinno być”.
  2. Zakres zapytań: główny feed bierze wpisy (post). CPT, strony i custom statusy nie wchodzą, dopóki nie zmienisz pre_get_posts.
  3. Treść vs skrót: opcja rss_use_excerpt (Ustawienia, Czytanie) decyduje o kształcie pozycji. Szablon RSS2 zawsze wypisuje <description> ze skrótem (the_excerpt_rss()), a przy pełnym tekście dokłada <content:encoded> z treścią z get_the_content_feed(). Filtry the_content_feed i the_excerpt_rss działają na tych dwóch polach.

Jeśli budujesz importer newslettera, sprawdź najpierw /feed/ bez wtyczek. Często problemem nie jest “brak RSS”, tylko brak miniatury, brak CPT albo zbyt agresywne CDN cache’ujące XML jak HTML.

Hooki warstwy XML, które warto znać zanim piszesz własny szablon: rss2_ns (namespace w korzeniu), rss2_head (dzieci <channel>), rss2_item (dzieci każdej <item>), oraz odpowiedniki Atom (atom_ns, atom_head, atom_entry). Filtry treści (the_content_feed, the_excerpt_rss) działają wcześniej, na stringu HTML wklejanym do pozycji. Kolejność ma znaczenie: najpierw budujesz treść pozycji, potem dokładasz elementy XML wokół niej.

Liczbę pozycji w głównym feedzie steruje opcja posts_per_rss (Ustawienia, Czytanie): WP_Query podstawia ją jako posts_per_page dla zapytań feedu, a ten sam limit obcina feed komentarzy. Nie podnoś jej do setek “na wszelki wypadek”: ciężki XML z pełną treścią i shortcode’ami zabija TTFB i psuje limity pamięci PHP na shared hostingu.

#Co szablon RSS2 WordPressa wysyła według specyfikacji RSS 2.0

Specyfikacja RSS 2.0 wymaga w kanale trzech elementów: title, link i description. W pozycji wszystkie elementy są opcjonalne, ale co najmniej jeden z title albo description musi być obecny. Daty mają format z RFC 822. Szablon feed-rss2.php spełnia to minimum i dokłada kilka elementów, o których warto wiedzieć, zanim zaczniesz je dublować w hookach:

  • <atom:link rel="self"> z adresem samego feedu (przestrzeń nazw Atom jest zadeklarowana w korzeniu RSS2),
  • <lastBuildDate> i <language> z ustawień witryny,
  • <sy:updatePeriod> i <sy:updateFrequency> z modułu Syndication, domyślnie hourly i 1, zmieniane filtrami rss_update_period i rss_update_frequency,
  • w każdej pozycji <dc:creator>, kategorie, <pubDate> w strefie +0000 i <guid isPermaLink="false">.

Element <ttl> (liczba minut, przez które kanał można cache’ować) nie występuje w szablonie core. Jeśli Twój czytelnik albo agregator go respektuje, dodajesz go na rss2_head.

guid zasługuje na osobną uwagę. Specyfikacja opisuje go jako ciąg jednoznacznie identyfikujący pozycję; isPermaLink domyślnie wynosi true, a przy false czytnik nie może zakładać, że to URL. WordPress wypisuje pole guid wpisu, ustalone przy tworzeniu (zwykle w formie https://domena/?p=123), i oznacza je jako isPermaLink="false". To poprawny wybór: zmiana sluga albo domeny nie zmienia identyfikatora, więc czytniki nie pokazują starych wpisów jako nowych. Nie nadpisuj guid przy migracji domeny, jeśli nie chcesz, żeby subskrybenci dostali powtórkę całego archiwum.

Specyfikacja zawiera też <enclosure> z trzema wymaganymi atrybutami: url, length (rozmiar w bajtach) i type (typ MIME). Szablon RSS2 woła rss_enclosure(), które czyta pole własne enclosure wpisu (URL, długość i typ w kolejnych liniach) i wypisuje gotowy element. Zanim dopiszesz własny enclosure w rss2_item, sprawdź, czy wpis nie ma już tego pola, bo dostaniesz duplikat.

#Czym różni się Atom w WordPressie od RSS 2.0

Atom jest opisany w RFC 4287 z 2005 roku i jest ściślejszy niż RSS. Element atom:feed musi zawierać dokładnie jeden atom:id, jeden atom:title i jeden atom:updated, a także co najmniej jednego atom:author, chyba że każdy wpis ma własnego autora. Te same trzy elementy są obowiązkowe w każdym atom:entry. Feed powinien mieć atom:link z rel="self", daty muszą odpowiadać formatowi date-time z RFC 3339, a typ MIME to application/atom+xml.

Szablon feed-atom.php realizuje to tak: <id> kanału to adres feedu Atom, <id> wpisu to guid, każdy wpis ma <author>, a obok <published> wypisuje <updated> z datą ostatniej modyfikacji wpisu. To praktyczna różnica: RSS2 z core ma tylko pubDate, więc poprawka w opublikowanym tekście nie zostawia w nim śladu, a Atom sygnalizuje ją przez updated. Jeśli Twoi odbiorcy synchronizują zmiany w treści (archiwa, wewnętrzne wyszukiwarki, importery), podaj im /feed/atom/.

#Jak dodać miniatury i media do feedu RSS

Domyślny feed nie dokłada featured image do treści. Większość narzędzi newsletterowych szuka <img> w treści pozycji albo w media RSS. Minimalna, przewidywalna poprawka:

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' );

Przy pełnym tekście w feedzie oba filtry trafiają do tej samej pozycji (skrót w <description>, treść w <content:encoded>), więc obrazek pojawi się w obu polach. Większość czytników pokazuje content:encoded, a importery newsletterów często czytają description, dlatego warto zostawić oba filtry.

Dla czytników respektujących Media RSS możesz też dodać namespace xmlns:media="http://search.yahoo.com/mrss/" na rss2_ns i element media:content na rss2_item:

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' );

Nie mieszaj obu podejść bez potrzeby: jeden kanał, jedna konwencja, mniej niespodzianek w importerach.

#Opóźnienie publikacji w feedzie

Scraperzy i agregatory zaciągają /feed/ w sekundach po publikacji. Jeśli indeksujesz oryginał wolniej niż kopia, masz klasyczny spór o kanoniczność. Opóźnienie pozycji w zapytaniu feedu o godzinę daje crawlerowi własnej domeny przewagę czasową:

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 );

Filtr posts_where przekazuje obiekt zapytania jako drugi argument, więc warunek dotyczy tylko głównego zapytania feedu, a nie widgetów czy bloków renderowanych w tym samym żądaniu. Porównanie kolumny z gotową datą (zamiast TIMESTAMPDIFF na kolumnie) pozwala bazie użyć indeksu na post_date_gmt.

To nie zastępuje rel=canonical ani sitemapy. To tylko bufor czasowy na warstwie XML. Na stronach z częstymi poprawkami po publikacji ustaw dłuższy próg albo wykluczaj statusy robocze inaczej.

#Custom post types w głównym feedzie

Główne /feed/ czyta typ post. Portfolio, wydarzenia albo release notes siedzą poza listą, dopóki nie rozszerzysz zapytania:

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;
	}

	// Tylko główny feed, nie /author/.../feed/ ani komentarze.
	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' );

Warunek is_main_query() jest kluczowy: bez niego zmienisz każde zapytanie wykonywane w trakcie generowania feedu, łącznie z zapytaniami wtyczek. Osobny feed CPT (/feed/portfolio/) często jest czystszy niż mieszanie typów w jednym XML. Typ treści zarejestrowany z has_archive ma też własny feed pod adresem archiwum, na przykład /portfolio/feed/.

#Jak utworzyć własny kanał RSS przez add_feed

add_feed( $feedname, $callback ) rejestruje rewrite /feed/{nazwa}/ i woła callback zamiast standardowego szablonu RSS2. To oficjalne API z dokumentacji feeds i referencji add_feed. Callback dostaje dwa argumenty: informację, czy to feed komentarzy, i nazwę feedu.

Wzorzec produkcyjny:

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' ) ) . '"?' . '>';
			// Tu budujesz pełny dokument RSS2 albo ładujesz własny szablon.
			load_template( get_stylesheet_directory() . '/feed-releases.php' );
		}
	);
}
add_action( 'init', 'wppoland_register_release_feed' );

Po pierwszej rejestracji (albo zmianie nazwy feedu) zrób flush_rewrite_rules() raz, przy aktywacji wtyczki, nie na każdym init. Bez flusha dostaniesz 404 pod /feed/releases/, mimo że kod callbacku jest poprawny.

W callbacku:

  • ustaw Content-Type zanim wypiszesz bajt XML,
  • escape’uj każde pole użytkownika (esc_xml / esc_url / esc_html),
  • ogranicz zapytanie (posts_per_page, status publish, ewentualnie meta query),
  • nie wołaj ciężkich shortcode’ów bez cache.

Nagłówek w callbacku jest potrzebny z konkretnego powodu. Dla każdego żądania feedu WP::send_headers() ustawia Content-Type z funkcji feed_content_type(), która zna tylko rss, rss2, rss-http, atom i rdf. Dla nieznanej nazwy zwraca application/octet-stream. Albo nadpisujesz nagłówek w callbacku (drugi argument true), albo dopisujesz typ filtrem feed_content_type, który dostaje typ MIME i nazwę feedu.

Jeśli chcesz tylko wariant głównego RSS2 (inna liczba pozycji, inny typ), czasem wystarczy add_feed + pre_get_posts na is_feed( 'releases' ) i standardowy szablon. Własny plik szablonu dajesz, gdy XML ma inne namespace’y albo inną strukturę pozycji.

Nazwa feedu trafia do rewrite tagu feed. Uważaj na nazwy z core (rss2, atom, rdf, rss): add_feed usuwa domyślny handler do_feed_{nazwa} i podpina Twój callback, więc add_feed( 'rss2', ... ) po cichu podmienia główny feed całej witryny. Po zmianie nazwy stary URL przestaje działać, dopóki nie dodasz przekierowania w konfiguracji serwera albo w template_redirect. W testach lokalnych po add_feed wejdź w Ustawienia, Permalinki i zapisz bez zmian, jeśli nie masz flusha w aktywacji wtyczki. To ten sam mechanizm.

#Jak działa cache feedów w WordPressie

Tu są trzy różne mechanizmy, które łatwo pomylić: cache feedów zewnętrznych pobieranych przez WordPressa, odpowiedzi warunkowe dla Twoich feedów i cache na CDN.

Feedy zewnętrzne. fetch_feed() buduje obiekt SimplePie z adresu RSS albo Atom i zwraca go, a przy błędzie zwraca WP_Error. Przyjmuje też tablicę adresów i scala je w jeden kanał. Wynik trafia do transientów, a czas życia zmienia filtr wp_feed_cache_transient_lifetime (domyślnie 12 godzin). To nie jest cache Twojego /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;
	}
);

Sprawdzanie is_wp_error() nie jest formalnością: niedostępny serwer źródłowy albo zły XML to codzienność, a bez tego warunku widget wysypuje fatal error na froncie. Treść z SimplePie przechodzi przez sanitizer oparty o KSES, ale tytuły i linki i tak escape’ujesz przy wypisywaniu.

Twoje feedy i odpowiedzi 304. Core obsługuje żądania warunkowe dla feedów bez żadnej wtyczki. WP::send_headers() przy każdym żądaniu z ustawioną zmienną feed liczy Last-Modified z daty ostatniej modyfikacji wpisu (dla feedów komentarzy także komentarza), z niej generuje ETag, i gdy klient przyśle zgodne If-Modified-Since albo If-None-Match, odpowiada 304 i kończy żądanie. Dotyczy to też endpointów z add_feed, bo przechodzą przez tę samą zmienną. Własną logikę 304 piszesz tylko dla endpointów poza tym mechanizmem, na przykład w REST API.

Własny endpoint /feed/ albo /feed/releases/ na brzegu:

  • CDN / reverse proxy: ustaw Cache-Control świadomie. Publiczny feed z max-age=300 zwykle wystarcza. Po publikacji invalidate’uj ścieżkę /feed/ albo wersjonuj ETag.
  • Object cache: jeśli budujesz ciężki JSON albo łączysz CPT z meta, cache’uj string odpowiedzi w wp_cache_set z krótkim TTL i czyść go na transition_post_status / save_post.
  • Nie cache’uj per-user HTML: feed jest publiczny; personalizacja w XML kończy się wyciekiem danych między subskrybentami.
  • Page cache wtyczek: niektóre pełne cache’e HTML serwują /feed/ jako stronę z niewłaściwym Content-Type. Wyklucz ścieżki feed z reguł cache albo wymuś bypass dla is_feed().

Przykład krótkiego nagłówka cache przy własnym feedzie:

header( 'Cache-Control: public, max-age=300' );
header( 'X-Robots-Tag: noindex' ); // opcjonalnie, jeśli nie chcesz indeksować XML

noindex na feedzie nie wyłącza syndykacji. Blokuje tylko indeksowanie samego XML jako “strony”. Przy Cloudflare albo innym edge: reguła cache dla *.xml / path /feed* z krótkim TTL i Cache Everything tylko wtedy, gdy Origin oddaje poprawny typ MIME. Zły edge cache potrafi trzymać pusty dokument po flushu rewrite’ów przez godziny.

#Jak bezpiecznie wyłączyć lub ograniczyć feedy RSS

Wyłączenie feedów “na sztywno” bez planu zostawia wiszące linki discovery i stare zakładki czytników. Bezpieczna kolejność:

  1. Usuń linki discovery: remove_action( 'wp_head', 'feed_links', 2 ) i remove_action( 'wp_head', 'feed_links_extra', 3 ), z priorytetami, z którymi core je rejestruje.
  2. Zdecyduj, czy stare URL-e mają 301 na treść HTML, pusty 410, albo nadal serwować XML.
  3. Dopiero potem wyłącz handlery do_feed_*.

Core rejestruje cztery handlery: do_feed_rdf, do_feed_rss, do_feed_rss2 i do_feed_atom. Osobnych handlerów dla komentarzy nie ma: /comments/feed/ przechodzi przez do_feed_rss2 (albo do_feed_atom) z pierwszym argumentem ustawionym na true. Minimalne odcięcie klasycznych feedów (zostawia możliwość własnego add_feed):

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__( 'Ten kanał został wyłączony.', 'wppoland' ),
		'',
		array( 'response' => 410 )
	);
}
add_action( 'do_feed_rss2', 'wppoland_feed_gone', 1 );
add_action( 'do_feed_atom', 'wppoland_feed_gone', 1 );

RDF i RSS 0.92 bez handlera kończą się 404 z do_feed(), RSS2 i Atom (razem z ich wersjami komentarzowymi) dostają 410.

Alternatywa łagodniejsza: zostaw RSS2, wytnij Atom/RDF i feed komentarzy. Agregatory i tak biorą RSS2. Sam feed komentarzy wyłączasz, sprawdzając argument handlera:

declare(strict_types=1);

function wppoland_comments_feed_gone( bool $is_comment_feed ): void {
	if ( $is_comment_feed ) {
		wp_die( esc_html__( 'Kanał komentarzy jest wyłączony.', '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' );

Filtr feed_links_show_comments_feed usuwa link discovery do kanału komentarzy z <head>. Nie usuwaj rewrite’ów feedów bez flusha, inaczej masz 404 zamiast kontrolowanej odpowiedzi.

#Jak feed autora ujawnia użytkowników WordPressa

/author/admin/feed/ i /author/mariusz/feed/ potwierdzają istnienie loginu. Razem z /?author=1 (redirect na archiwum) to klasyczny wektor enumeracji kont przed brute-force albo phishingiem celowanym.

Twarde domknięcie archiwów autora (w tym feedów):

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' );

Jeśli archiwa autora mają zostać (blog personalny), przynajmniej:

  • używaj ładnego nicename, nie surowego loginu w URL, jeśli to możliwe w Twoim modelu,
  • nie publikuj listy autorów z loginami w HTML,
  • rozważ ograniczenie feedu autora do pustej listy albo 404 tylko dla is_feed() && is_author().

Pamiętaj, że szablon RSS2 wypisuje w każdej pozycji <dc:creator> z nazwą wyświetlaną autora, a Atom element <author>. Jeśli nazwa wyświetlana jest równa loginowi, główny feed ujawnia go tak samo jak archiwum autora.

REST API (/wp-json/wp/v2/users) to osobny temat. Wyłączenie feedu autora nie zamyka enumeracji przez REST bez dodatkowych filtrów. Traktuj RSS jako jedną z wielu powierzchni ujawniających tożsamość.

Nie wkładaj do feedu adresów e-mail, tokenów, wewnętrznych ID zamówień ani treści private / draft. Filtry treści działają też w kontekście feedu: shortcode, który w HTML pokazuje panel klientowi, w XML może wyciec do subskrybentów publicznych.

Komentarzowy feed (/comments/feed/ albo feed pod pojedynczym wpisem) bywa pomijany w audytach. Jeśli komentarze zawierają treść logowania, numery zamówień albo PII klientów B2B, wyłącz feed komentarzy kodem z poprzedniej sekcji albo trzymaj moderację zanim komentarz trafi do XML. Publiczny kanał komentarzy to osobna powierzchnia scrapingu, nie tylko “bonus dla czytelników”.

#Jak zrobić feed podcastu z iTunes i enclosure

WordPress nie jest hostem plików audio, ale może być generatorem RSS zgodnym z katalogami. Wymagania Apple Podcasts mówią wprost: każdy odcinek musi mieć unikalny <enclosure> z trzema składnikami (URL, długość, typ), każdy odcinek musi mieć GUID, który nigdy się nie zmienia, a feed musi zawierać co najmniej jeden odcinek. Listę wymaganych tagów kanału Apple trzyma w osobnym dokumencie z tagami RSS i to z nim porównujesz feed przed zgłoszeniem.

Technicznie potrzebujesz:

  • namespace xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" (historyczny URL namespace; katalogi go rozpoznają),
  • tagów kanału z przestrzeni itunes (na przykład itunes:author, itunes:category, itunes:image) oraz language,
  • pozycji z <enclosure url="..." length="..." type="audio/mpeg" /> oraz opcjonalnie itunes:duration.

Hooki 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' );

Ten kod zakłada, że odcinki nie mają wypełnionego pola enclosure, które rss_enclosure() wypisuje automatycznie. Funkcja esc_xml() jest w core od wersji 5.5 i jest właściwym escape’em dla tekstu w elementach XML.

Dla produkcji zwykle lepiej osobny feed (add_feed( 'podcast', ... )) niż mieszanie zwykłych wpisów z odcinkami. Katalogi walidują cały kanał; jeden zły item potrafi odrzucić submisję. Trzymaj MP3 na stabilnym URL (CDN), z poprawnym length w bajtach, bo zgadywanie długości psuje klientów. Apple zaleca też włączenie na serwerze plików żądań HEAD i żądań z zakresem bajtów, od których zależy odtwarzanie strumieniowe.

#Jak dodać JSON Feed do WordPressa

JSON Feed to alternatywa XML dla klientów JS. WordPress core nie rejestruje go natywnie. Dokładasz go przez add_feed( 'json', $callback ) i nagłówek Content-Type: application/feed+json, bo taki typ MIME zaleca specyfikacja.

Szkielet callbacku:

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 nie zastępuje RSS w podcastach ani w starszych czytnikach. Traktuj go jako drugi kontrakt dla własnych klientów i narzędzi wewnętrznych. Discovery w HTML: type="application/feed+json".

Specyfikacja wymaga na poziomie kanału pól version, title i items, a w każdej pozycji id (string) oraz co najmniej jednego z content_html albo content_text. Wersja 1.1 dodała pole authors (tablica, wielu autorów na kanał albo pozycję) i pole language, a pojedyncze author oznaczyła jako przestarzałe. Trzymaj id pozycji stabilny: specyfikacja mówi, że po aktualizacji pozycji id nie powinno się zmieniać, a nowe pozycje nie powinny używać wcześniej użytego id. Dlatego w przykładzie id to guid, a nie permalink, który zmienia się razem ze slugiem. content_text albo content_html: wybierz jedno jako źródło prawdy; podawanie obu z rozjechaną treścią kończy się niespójnym podglądem w klientach.

#Jak włączyć WebSub dla feedu WordPressa

Czytnik bez WebSub odpytuje feed w swoim rytmie, więc nowy wpis dociera do subskrybentów z opóźnieniem zależnym od czytnika. WebSub, rekomendacja W3C, odwraca kierunek: wydawca wskazuje hub, subskrybenci rejestrują się w hubie, a hub rozsyła aktualizację, gdy wydawca da znać o zmianie.

Wymagania po stronie wydawcy są krótkie. Specyfikacja zaleca nagłówek Link z co najmniej jednym rel="hub" i dokładnie jednym rel="self", a dla feedów XML osadzone elementy link. Wydawca musi poinformować wskazane huby o aktualizacji tematu, przy czym sam mechanizm powiadomienia specyfikacja zostawia otwarty. W nocie podaje przykład spotykany w publicznych hubach: żądanie POST z kluczami hub.mode=publish i hub.url wskazującym zmieniony zasób.

Core WordPressa nie implementuje WebSub. W katalogu wtyczek jest wtyczka “WebSub (FKA. PubSubHubbub)”, która dodaje oba elementy. Ręcznie wygląda to tak:

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" już jest w szablonie RSS2 (<atom:link rel="self">), więc dokładasz tylko rel="hub". Adres huba i format powiadomienia sprawdź w dokumentacji wybranego huba, bo specyfikacja ich nie narzuca. Weryfikację subskrypcji (hub.challenge), wygasanie po hub.lease_seconds i podpis X-Hub-Signature przy sekrecie obsługuje hub i subskrybent, nie WordPress.

#Jak naprawić błąd parsowania XML w feedzie WordPressa

Najczęstszy błąd przy własnych szablonach i wtyczkach to dokument, który czytnik odrzuca komunikatem w rodzaju XML or text declaration not at start of entity. Deklaracja <?xml ... ?> musi być pierwszym znakiem odpowiedzi. Wystarczy pusta linia albo znacznik BOM wypisany przed nią, a parser odmawia przetworzenia całego kanału.

Przyczyna prawie zawsze leży w pliku PHP ładowanym przy każdym żądaniu: spacja albo nowa linia po zamykającym ?> w functions.php lub we wtyczce, albo plik zapisany w UTF-8 z BOM. Właściwa poprawka to usunięcie zamykającego ?> z plików zawierających wyłącznie PHP i zapis plików bez BOM. Żeby znaleźć winowajcę, zrób curl -s https://domena/feed/ | head -c 100 | xxd i zobacz, co stoi przed <?xml, a potem wyłączaj wtyczki po kolei.

Jeśli nie możesz od razu poprawić cudzej wtyczki, tymczasowo pomoże bufor wyjścia czyszczący początek odpowiedzi feedu:

declare(strict_types=1);

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

	ob_start(
		static function ( string $buffer ): string {
			// Usuń BOM i białe znaki przed deklaracją XML.
			return ltrim( preg_replace( '/^\xEF\xBB\xBF/', '', $buffer ) );
		}
	);
}
add_action( 'template_redirect', 'wppoland_clean_feed_output', 1 );

To łata, nie naprawa: bufor nie usunie znaków wypisanych, zanim template_redirect się wykona. Zespoły z testami integracyjnymi mogą pilnować tego automatycznie. Testy feedów w samym core ładują szablon bezpośrednio i tłumią ostrzeżenia o nagłówkach; ten sam wzorzec działa w 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 ) );
	}
}

Asercja sprawdza surowy wynik bez trim(), bo właśnie białe znaki na początku są tu błędem.

#Jak prowadzić feedy RSS w wielojęzycznym WordPressie

Przy WPML, Polylangu albo własnych prefiksach języka WordPress zwykle wystawia kilka adresów feedu. Typowe błędy:

  • główny feed miesza wersje językowe, bo pre_get_posts nie filtruje języka,
  • CDN cache’uje /feed/ bez rozróżnienia ścieżki albo ciasteczka i podaje treść jednego języka subskrybentom drugiego,
  • adresy w enclosure wskazują złą domenę po mapowaniu domen,
  • element <language> w kanale pochodzi z ustawień witryny, a nie z języka treści konkretnego feedu.

Trzymaj jeden jasny endpoint na język, filtruj język albo taksonomię wcześnie w zapytaniu i testuj feed czytnikiem, który naprawdę wysyła If-None-Match, a nie tylko kartą przeglądarki. Przy kilku markach w jednej instalacji osobny add_feed( 'marka-a', ... ) jest czystszy niż parametr zapytania doklejany do głównego feedu: parametry rozbijają cache po stronie CDN, a ETag z core zależy wyłącznie od daty ostatniej modyfikacji, nie od parametrów.

#Co sprawdzić przed wdrożeniem własnego feedu RSS

Zanim wypchniesz zmiany na produkcję:

  1. Otwórz /feed/ w przeglądarce i w curl -I: status 200, sensowny Content-Type.
  2. Po add_feed aktywuj wtyczkę / flush rewrite i sprawdź nowy URL.
  3. Usuń zbędne rel=alternate, jeśli wyłączasz feedy.
  4. Sprawdź /author/.../feed/ pod kątem enumeracji.
  5. Jeśli robisz podcast, przepuść XML przez walidator katalogu przed submisją.
  6. Po publikacji testowego wpisu potwierdź, że CDN oddaje świeży XML w akceptowalnym TTL.
  7. Zweryfikuj, że shortcode’y i bloki dynamiczne w treści feedu nie wypuszczają HTML panelu admina ani danych sesji.
  8. Wyślij drugie żądanie z nagłówkiem If-None-Match ustawionym na otrzymany ETag i sprawdź, czy wraca 304.

RSS w WordPressie to warstwa API, nie relikt. Kontrolujesz zapytanie, szablon, cache i powierzchnię ataku tak samo jak endpoint REST. Różnica jest taka, że klienci feedu są starsi, mniej wyrozumiali na zły XML i chętniej cache’ują odpowiedź po swojej stronie.

Oficjalne punkty odniesienia: Feeds (Developer Resources), add_feed oraz WordPress Feeds w dokumentacji administracyjnej. Zanim napiszesz kolejną wtyczkę “RSS Pack”, sprawdź, którego z tych trzech kontraktów naprawdę brakuje w Twoim motywie.

Jeśli potrzebujesz rozszerzyć feed o własne typy treści, pola meta albo osobny kanał podcastu w ramach programowania pod WordPressa, zacznij od jednego add_feed i jednego szablonu, bez monolitowej wtyczki “all feeds”.

Następny krok

Przekuj artykuł w realne wdrożenie

Pod tym wpisem dokładam linki, które domykają intencję użytkownika i prowadzą dalej w strukturze serwisu.

Chcesz wdrożyć ten temat na swojej stronie?

Jeśli chcesz przełożyć wiedzę z artykułu na działającą stronę, sklep albo przebudowę serwisu, przygotuję konkretny zakres prac.

Powiązany klaster

Sprawdź inne usługi WordPress i bazę wiedzy

Wzmocnij swój biznes dzięki profesjonalnemu wsparciu technicznemu w kluczowych obszarach ekosystemu WordPress.

Polecane artykuły