Mistrzowskie kanały RSS w WordPressie: przewodnik dewelopera

Mistrzowskie kanały RSS w WordPressie: przewodnik dewelopera

Ostatnio zweryfikowano: 21 września 2026
11 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, add_feed, cache, bezpieczne wyłączanie, enumerację użytkowników przez feed autora oraz praktyczne uwagi o podcastach i JSON Feed.

#Domyślne kanały WordPressa

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
  • /kategoria/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). Szablon renderujący wybiera do_feed_* i pliki z wp-includes/feed-*.php.

W praktyce produkcyjnej trzy rzeczy są ważne:

  1. Discovery - motyw wypisuje <link rel="alternate" type="application/rss+xml" ...> w wp_head. 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 - ustawienie “pełny tekst / skrót w feedzie” w Ustawienia → Czytanie steruje tym, co ląduje w <description> / content. Filtry the_content_feed i the_excerpt_rss działają potem.

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) albo pre_option_posts_per_rss / pre_get_posts. 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.

#Wzbogacanie treści: miniatury i media

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

Dla czytników respektujących Media RSS możesz też dodać namespace i element media:content na rss2_item. 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 ): string {
	global $wpdb;

	if ( ! is_feed() ) {
		return $where;
	}

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

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

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

Sprawdzaj is_main_query(). Stary wzorzec z ! $query->is_main_query() w filtrze feedu jest odwrotny i w praktyce nie robi tego, czego oczekujesz. Osobny feed CPT (/feed/portfolio/) często jest czystszy niż mieszanie typów w jednym XML.

#Własny kanał 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.

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.

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. Unikaj kolizji z zarezerwowanymi wartościami (rss2, atom, rdf, rss). Po zmianie nazwy stary URL przestaje działać, dopóki nie dodasz redirectu w _redirects 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.

#Cache feedów

WordPress cache’uje zewnętrzne feedy pobierane przez SimplePie (fetch_feed) w transientach. Czas życia zmienia filtr wp_feed_cache_transient_lifetime (domyślnie 12 godzin). To nie jest cache Twojego /feed/.

Dla fetch_feed (widgety “ostatnie z innego bloga”, importy):

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

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

  • 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.

#Bezpieczne usuwanie i ograniczanie feedów

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

  1. Usuń albo zmień <link rel="alternate" ...> w motywie / przez remove_action na hookach discovery.
  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_*.

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 );
	remove_action( 'do_feed_rss2_comments', 'do_feed_rss2_comments', 10 );
	remove_action( 'do_feed_atom_comments', 'do_feed_atom_comments', 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 );

Alternatywa łagodniejsza: zostaw RSS2, wytnij Atom/RDF i feed komentarzy. Agregatory i tak biorą RSS2. Nie usuwaj rewrite’ów feedów bez flusha - inaczej masz 404 zamiast kontrolowanej odpowiedzi.

#Bezpieczeństwo: enumeracja użytkowników przez feed autora

/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().

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 jeden 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 tymi samymi remove_action co wyżej albo trzymaj moderację zanim komentarz trafi do XML. Publiczny kanał komentarzy to osobna powierzchnia scrapingu, nie tylko “bonus dla czytelników”.

#Podcast: namespace iTunes i enclosure

WordPress nie jest hostem plików audio, ale może być generatorem RSS zgodnym z katalogami. Apple i większość katalogów oczekują:

  • namespace xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" (historyczny URL namespace; katalogi go rozpoznają),
  • elementów kanału: itunes:author, itunes:summary, itunes:category, itunes:image, 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_html( get_bloginfo( 'name' ) ) . '</itunes:author>';
	echo '<itunes:summary>' . esc_html( get_bloginfo( 'description' ) ) . '</itunes:summary>';
}
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' );

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 - zgadywanie długości psuje klientów.

#JSON Feed

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.

Szkielet callbacku:

declare(strict_types=1);

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

	$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'             => get_permalink( $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 ),
		);
	}

	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/' ),
			'items'         => $items,
		),
		JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
	);
}

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".

Wersja 1.1 dopuszcza authors (tablica), language i attachments zamiast polegacza wyłącznie na author / enclosure. Trzymaj id pozycji stabilny (permalink albo GUID), bo czytnik traktuje zmianę id jako nową pozycję i potrafi zdublować powiadomienia. 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.

#Checklist wdrożenia

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.

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 użytkownika. 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