Dominar os feeds RSS no WordPress: guia do programador

Dominar os feeds RSS no WordPress: guia do programador

Última verificação: 12 de outubro de 2026
26 min de leitura
Guia
Desenvolvedor full-stack

O WordPress serve conteúdo em XML há anos, desde antes de alguém inventar o termo “headless CMS”. O feed RSS continua a ser o contrato mais simples: uma lista de itens, título, conteúdo ou excerto, data e ligação canónica. Agregadores, importadores de newsletters, leitores e diretórios de podcasts continuam a consumi-lo.

Este texto destina-se ao programador que mantém um tema ou um pequeno plugin e quer controlar os feeds sem um pacote “RSS everything”. Cobre os endpoints predefinidos, aquilo que o template RSS2 envia de facto face à especificação, add_feed, cache, desativação segura, a enumeração de utilizadores através do feed de autor, podcasts, JSON Feed e WebSub. As passagens sobre o comportamento do core foram verificadas no código-fonte do WordPress (wp-includes/feed-rss2.php, feed-atom.php, feed.php, class-wp.php).

#Que feeds RSS predefinidos tem o WordPress

O core regista vários tipos de feed. Com as ligações permanentes “bonitas” ativas, os mais comuns são:

  • /feed/ ou /feed/rss2/: RSS 2.0 (predefinido)
  • /feed/atom/: Atom
  • /feed/rdf/: RDF/RSS 1.0
  • /comments/feed/: comentários globais
  • /category/slug/feed/, /tag/slug/feed/, /author/login/feed/, /search/termo/feed/: feeds de contexto

Sem ligações permanentes “bonitas”, os mesmos dados saem por query string (?feed=rss2). A função do_feed() lê a variável feed e dispara a ação do_feed_{nome}; os handlers predefinidos carregam os ficheiros wp-includes/feed-*.php. Se não existir nenhuma ação para esse nome, o WordPress termina o pedido com um erro 404.

Em produção, contam três coisas:

  1. Discovery: o core escreve <link rel="alternate" type="application/rss+xml" ...> no wp_head através das funções feed_links e feed_links_extra, desde que o tema declare add_theme_support( 'automatic-feed-links' ). O leitor e o crawler usam o que veem no HTML, não o que “deveria estar lá”.
  2. Âmbito da consulta: o feed principal inclui artigos (post). CPT, páginas e estados personalizados ficam de fora até alterar o pre_get_posts.
  3. Conteúdo vs excerto: a opção rss_use_excerpt (Definições, Leitura) define a forma de cada item. O template RSS2 escreve sempre <description> com o excerto (the_excerpt_rss()) e, com texto completo, acrescenta <content:encoded> com o conteúdo de get_the_content_feed(). Os filtros the_content_feed e the_excerpt_rss atuam sobre estes dois campos.

Se está a construir um importador de newsletter, comece por verificar /feed/ sem plugins. Muitas vezes o problema não é “falta de RSS”, mas sim falta de miniatura, falta de CPT ou uma CDN demasiado agressiva que guarda o XML em cache como se fosse HTML.

Hooks da camada XML que convém conhecer antes de escrever um template próprio: rss2_ns (namespaces na raiz), rss2_head (filhos de <channel>), rss2_item (filhos de cada <item>) e os equivalentes Atom (atom_ns, atom_head, atom_entry). Os filtros de conteúdo (the_content_feed, the_excerpt_rss) atuam antes, sobre a string HTML inserida no item. A ordem importa: primeiro constrói o conteúdo do item, depois acrescenta os elementos XML à volta dele.

O número de itens no feed principal é controlado pela opção posts_per_rss (Definições, Leitura): o WP_Query usa-a como posts_per_page nas consultas de feed, e o mesmo limite corta o feed de comentários. Não a suba para centenas “por precaução”: um XML pesado, com conteúdo completo e shortcodes, destrói o TTFB e rebenta os limites de memória do PHP em alojamento partilhado.

#O que o template RSS2 do WordPress envia segundo a especificação RSS 2.0

A especificação RSS 2.0 exige três elementos no canal: title, link e description. No item, todos os elementos são opcionais, mas pelo menos um de title ou description tem de estar presente. As datas seguem o formato do RFC 822. O template feed-rss2.php cumpre este mínimo e acrescenta alguns elementos que convém conhecer antes de os duplicar em hooks:

  • <atom:link rel="self"> com o endereço do próprio feed (o namespace Atom está declarado na raiz do RSS2),
  • <lastBuildDate> e <language> a partir das definições do site,
  • <sy:updatePeriod> e <sy:updateFrequency> do módulo Syndication, por predefinição hourly e 1, alteráveis com os filtros rss_update_period e rss_update_frequency,
  • em cada item, <dc:creator>, categorias, <pubDate> no fuso +0000 e <guid isPermaLink="false">.

O elemento <ttl> (número de minutos durante os quais o canal pode ficar em cache) não existe no template do core. Se o seu leitor ou agregador o respeita, acrescente-o no rss2_head.

O guid merece atenção à parte. A especificação descreve-o como uma string que identifica o item de forma inequívoca; isPermaLink vale true por predefinição e, com false, o leitor não pode assumir que se trata de um URL. O WordPress escreve o campo guid do artigo, definido no momento da criação (normalmente na forma https://dominio/?p=123), e marca-o como isPermaLink="false". É a escolha certa: mudar o slug ou o domínio não altera o identificador, por isso os leitores não mostram artigos antigos como novos. Não reescreva o guid numa migração de domínio, a não ser que queira que os subscritores recebam outra vez todo o arquivo.

A especificação inclui também <enclosure> com três atributos obrigatórios: url, length (tamanho em bytes) e type (tipo MIME). O template RSS2 chama rss_enclosure(), que lê o campo personalizado enclosure do artigo (URL, tamanho e tipo em linhas sucessivas) e escreve o elemento pronto. Antes de acrescentar um enclosure próprio no rss2_item, confirme que o artigo ainda não tem esse campo, senão obtém um duplicado.

#Diferença entre Atom e RSS 2.0 no WordPress

O Atom está descrito no RFC 4287, de 2005, e é mais rigoroso do que o RSS. O elemento atom:feed tem de conter exatamente um atom:id, um atom:title e um atom:updated, além de pelo menos um atom:author, a não ser que cada entrada tenha o seu próprio autor. Os mesmos três elementos são obrigatórios em cada atom:entry. O feed deve ter um atom:link com rel="self", as datas têm de seguir o formato date-time do RFC 3339 e o tipo MIME é application/atom+xml.

O template feed-atom.php resolve isto assim: o <id> do canal é o endereço do feed Atom, o <id> da entrada é o guid, cada entrada tem <author> e, ao lado de <published>, escreve <updated> com a data da última modificação do artigo. A diferença prática é esta: o RSS2 do core só tem pubDate, pelo que uma correção num texto já publicado não deixa rasto, enquanto o Atom a sinaliza através de updated. Se os seus destinatários sincronizam alterações de conteúdo (arquivos, motores de pesquisa internos, importadores), indique-lhes /feed/atom/.

#Como adicionar miniaturas e media ao feed RSS

O feed predefinido não acrescenta a imagem destacada ao conteúdo. A maioria das ferramentas de newsletter procura um <img> no conteúdo do item ou em Media RSS. Uma correção mínima e previsível:

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

Com texto completo no feed, os dois filtros chegam ao mesmo item (excerto em <description>, conteúdo em <content:encoded>), por isso a imagem aparece nos dois campos. A maioria dos leitores mostra content:encoded, enquanto os importadores de newsletters leem muitas vezes description, e por isso compensa manter ambos os filtros.

Para leitores que respeitam Media RSS, pode também acrescentar o namespace xmlns:media="http://search.yahoo.com/mrss/" no rss2_ns e o elemento media:content no 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' );

Não misture as duas abordagens sem necessidade: um canal, uma convenção, menos surpresas nos importadores.

#Como atrasar a publicação no feed do WordPress

Scrapers e agregadores puxam o /feed/ segundos depois da publicação. Se o original é indexado mais devagar do que a cópia, tem a clássica disputa de canonicidade. Atrasar os itens na consulta do feed em uma hora dá ao crawler do seu próprio domínio vantagem temporal:

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

O filtro posts_where recebe o objeto da consulta como segundo argumento, por isso a condição aplica-se apenas à consulta principal do feed, e não a widgets ou blocos renderizados no mesmo pedido. Comparar a coluna com uma data já calculada (em vez de aplicar TIMESTAMPDIFF à coluna) permite à base de dados usar o índice em post_date_gmt.

Isto não substitui o rel=canonical nem o sitemap. É apenas uma margem temporal na camada XML. Em sites com correções frequentes após a publicação, defina um limite mais longo ou exclua os estados de rascunho de outra forma.

#Como incluir custom post types no feed principal

O /feed/ principal lê o tipo post. Portefólio, eventos ou notas de versão ficam fora da lista até alargar a consulta:

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

	// Apenas o feed principal, não /author/.../feed/ nem comentários.
	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' );

A condição is_main_query() é essencial: sem ela, altera todas as consultas executadas durante a geração do feed, incluindo as dos plugins. Um feed CPT separado (/feed/portfolio/) é muitas vezes mais limpo do que misturar tipos no mesmo XML. Um tipo de conteúdo registado com has_archive tem também o seu próprio feed no endereço do arquivo, por exemplo /portfolio/feed/.

#Como criar um feed RSS personalizado com add_feed

add_feed( $feedname, $callback ) regista a reescrita /feed/{nome}/ e chama o callback em vez do template RSS2 padrão. É a API oficial descrita na documentação de feeds e na referência de add_feed. O callback recebe dois argumentos: a indicação de se é um feed de comentários e o nome do feed.

Padrão para produção:

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' ) ) . '"?' . '>';
			// Aqui constrói o documento RSS2 completo ou carrega um template próprio.
			load_template( get_stylesheet_directory() . '/feed-releases.php' );
		}
	);
}
add_action( 'init', 'wppoland_register_release_feed' );

Após o primeiro registo (ou a mudança de nome do feed), execute flush_rewrite_rules() uma única vez, na ativação do plugin, e não em cada init. Sem o flush, obtém um 404 em /feed/releases/, mesmo com o código do callback correto.

No callback:

  • defina o Content-Type antes de escrever o primeiro byte de XML,
  • faça escape de cada campo do utilizador (esc_xml / esc_url / esc_html),
  • limite a consulta (posts_per_page, estado publish, eventualmente uma meta query),
  • não chame shortcodes pesados sem cache.

O cabeçalho no callback é necessário por uma razão concreta. Em cada pedido de feed, WP::send_headers() define o Content-Type a partir da função feed_content_type(), que só conhece rss, rss2, rss-http, atom e rdf. Para um nome desconhecido, devolve application/octet-stream. Ou sobrepõe o cabeçalho no callback (segundo argumento true), ou acrescenta o tipo com o filtro feed_content_type, que recebe o tipo MIME e o nome do feed.

Se só precisa de uma variante do RSS2 principal (outro número de itens, outro tipo), às vezes basta add_feed + pre_get_posts em is_feed( 'releases' ) e o template padrão. Um ficheiro de template próprio justifica-se quando o XML tem outros namespaces ou outra estrutura de itens.

O nome do feed entra na tag de reescrita feed. Cuidado com os nomes do core (rss2, atom, rdf, rss): o add_feed remove o handler predefinido do_feed_{nome} e liga o seu callback, por isso add_feed( 'rss2', ... ) substitui sem aviso o feed principal de todo o site. Depois de mudar o nome, o URL antigo deixa de funcionar até acrescentar um redirecionamento na configuração do servidor ou no template_redirect. Em testes locais, depois de add_feed, abra Definições, Ligações permanentes e guarde sem alterações, se não tiver o flush na ativação do plugin. É o mesmo mecanismo.

#Como funciona a cache de feeds no WordPress

Há aqui três mecanismos distintos, fáceis de confundir: a cache dos feeds externos obtidos pelo WordPress, as respostas condicionais para os seus próprios feeds e a cache na CDN.

Feeds externos. fetch_feed() constrói um objeto SimplePie a partir de um endereço RSS ou Atom e devolve-o, ou devolve WP_Error em caso de erro. Aceita também um array de endereços e junta-os num único canal. O resultado vai para transients e o tempo de vida é alterado pelo filtro wp_feed_cache_transient_lifetime (12 horas por predefinição). Isto não é a cache do seu /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;
	}
);

Verificar is_wp_error() não é formalidade: um servidor de origem indisponível ou um XML inválido acontecem todos os dias, e sem essa condição o widget atira um fatal error no front-end. O conteúdo do SimplePie passa por um sanitizador baseado no KSES, mas os títulos e as ligações devem ser escapados na mesma ao escrevê-los.

Os seus feeds e as respostas 304. O core trata os pedidos condicionais de feeds sem qualquer plugin. Em cada pedido com a variável feed definida, WP::send_headers() calcula o Last-Modified a partir da data da última modificação de um artigo (nos feeds de comentários, também de um comentário), gera dele o ETag e, quando o cliente envia um If-Modified-Since ou If-None-Match correspondente, responde 304 e termina o pedido. Isto aplica-se também aos endpoints criados com add_feed, porque passam pela mesma variável. Só precisa de escrever lógica 304 própria para endpoints fora deste mecanismo, por exemplo na REST API.

Endpoint próprio /feed/ ou /feed/releases/ na edge:

  • CDN / reverse proxy: defina o Cache-Control de forma consciente. Um feed público com max-age=300 costuma bastar. Após a publicação, invalide o caminho /feed/ ou versione o ETag.
  • Object cache: se constrói um JSON pesado ou junta CPT com meta, guarde a string da resposta com wp_cache_set e um TTL curto, e limpe-a em transition_post_status / save_post.
  • Não guarde HTML por utilizador em cache: o feed é público; personalização no XML acaba em fuga de dados entre subscritores.
  • Cache de página dos plugins: algumas caches de HTML completo servem o /feed/ como página com o Content-Type errado. Exclua os caminhos feed das regras de cache ou force o bypass para is_feed().

Exemplo de um cabeçalho de cache curto num feed próprio:

header( 'Cache-Control: public, max-age=300' );
header( 'X-Robots-Tag: noindex' ); // opcional, se não quiser indexar o XML

Um noindex no feed não desativa a sindicação. Bloqueia apenas a indexação do próprio XML como “página”. Com Cloudflare ou outra edge: uma regra de cache para *.xml / caminho /feed* com TTL curto e Cache Everything apenas quando a origem devolve o tipo MIME correto. Uma cache de edge mal configurada consegue manter um documento vazio durante horas depois de um flush das regras de reescrita.

#Como desativar ou limitar feeds RSS com segurança

Desativar os feeds “à força”, sem plano, deixa ligações de discovery penduradas e marcadores antigos nos leitores. A ordem segura:

  1. Remova as ligações de discovery: remove_action( 'wp_head', 'feed_links', 2 ) e remove_action( 'wp_head', 'feed_links_extra', 3 ), com as prioridades com que o core as regista.
  2. Decida se os URLs antigos recebem um 301 para conteúdo HTML, um 410 vazio, ou se continuam a servir XML.
  3. Só depois desative os handlers do_feed_*.

O core regista quatro handlers: do_feed_rdf, do_feed_rss, do_feed_rss2 e do_feed_atom. Não há handlers separados para comentários: /comments/feed/ passa por do_feed_rss2 (ou do_feed_atom) com o primeiro argumento definido como true. Corte mínimo dos feeds clássicos (mantém a possibilidade de um add_feed próprio):

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__( 'Este feed foi desativado.', 'wppoland' ),
		'',
		array( 'response' => 410 )
	);
}
add_action( 'do_feed_rss2', 'wppoland_feed_gone', 1 );
add_action( 'do_feed_atom', 'wppoland_feed_gone', 1 );

RDF e RSS 0.92, sem handler, terminam com um 404 vindo de do_feed(); RSS2 e Atom (incluindo as respetivas versões de comentários) recebem 410.

Alternativa mais suave: mantenha o RSS2 e corte Atom/RDF e o feed de comentários. Os agregadores usam o RSS2 de qualquer forma. Para desativar apenas o feed de comentários, verifique o argumento do handler:

declare(strict_types=1);

function wppoland_comments_feed_gone( bool $is_comment_feed ): void {
	if ( $is_comment_feed ) {
		wp_die( esc_html__( 'O feed de comentários está desativado.', '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' );

O filtro feed_links_show_comments_feed remove do <head> a ligação de discovery para o feed de comentários. Não remova as reescritas dos feeds sem flush, caso contrário obtém um 404 em vez de uma resposta controlada.

#Como o feed de autor expõe os utilizadores do WordPress

/author/admin/feed/ e /author/mariusz/feed/ confirmam a existência de um login. Juntamente com /?author=1 (redirecionamento para o arquivo), é o vetor clássico de enumeração de contas antes de um ataque de força bruta ou de phishing dirigido.

Fecho rígido dos arquivos de autor (incluindo os 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' );

Se os arquivos de autor tiverem de ficar (blogue pessoal), pelo menos:

  • use um nicename limpo, e não o login em bruto no URL, se o seu modelo o permitir,
  • não publique uma lista de autores com logins no HTML,
  • considere limitar o feed de autor a uma lista vazia ou a um 404 apenas para is_feed() && is_author().

Lembre-se de que o template RSS2 escreve em cada item um <dc:creator> com o nome de apresentação do autor, e o Atom um elemento <author>. Se o nome de apresentação for igual ao login, o feed principal revela-o tal como o arquivo de autor.

A REST API (/wp-json/wp/v2/users) é outro assunto. Desativar o feed de autor não fecha a enumeração através da REST sem filtros adicionais. Trate o RSS como uma entre várias superfícies que revelam identidades.

Não coloque no feed endereços de e-mail, tokens, IDs internos de encomendas nem conteúdo private / draft. Os filtros de conteúdo também correm no contexto do feed: um shortcode que no HTML mostra um painel ao cliente pode, no XML, chegar aos subscritores públicos.

O feed de comentários (/comments/feed/ ou o feed de um artigo individual) costuma escapar às auditorias. Se os comentários contêm dados de login, números de encomenda ou dados pessoais de clientes B2B, desative o feed de comentários com o código da secção anterior ou mantenha a moderação antes de o comentário chegar ao XML. Um canal público de comentários é uma superfície de scraping à parte, e não apenas “um bónus para os leitores”.

#Como criar um feed de podcast com iTunes e enclosure

O WordPress não é um alojamento de ficheiros de áudio, mas pode ser um gerador de RSS compatível com os diretórios. Os requisitos do Apple Podcasts são explícitos: cada episódio tem de ter um <enclosure> único com três componentes (URL, tamanho, tipo), cada episódio tem de ter um GUID que nunca muda e o feed tem de conter pelo menos um episódio. A lista de tags de canal obrigatórias está num documento separado da Apple sobre tags RSS, e é com ele que deve comparar o feed antes da submissão.

Tecnicamente, precisa de:

  • o namespace xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" (URL histórico do namespace; os diretórios reconhecem-no),
  • tags de canal do espaço itunes (por exemplo itunes:author, itunes:category, itunes:image) e language,
  • itens com <enclosure url="..." length="..." type="audio/mpeg" /> e, opcionalmente, itunes:duration.

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

Este código assume que os episódios não têm preenchido o campo enclosure, que o rss_enclosure() escreve automaticamente. A função esc_xml() existe no core desde a versão 5.5 e é o escape adequado para texto dentro de elementos XML.

Em produção, costuma ser melhor um feed separado (add_feed( 'podcast', ... )) do que misturar artigos normais com episódios. Os diretórios validam o canal inteiro; um item errado pode fazer rejeitar a submissão. Mantenha os MP3 num URL estável (CDN), com o length correto em bytes, porque adivinhar o tamanho estraga os clientes. A Apple recomenda também ativar no servidor de ficheiros os pedidos HEAD e os pedidos com intervalo de bytes, de que depende a reprodução em streaming.

#Como adicionar JSON Feed ao WordPress

O JSON Feed é uma alternativa ao XML para clientes JS. O core do WordPress não o regista de forma nativa. Acrescenta-se com add_feed( 'json', $callback ) e o cabeçalho Content-Type: application/feed+json, porque é esse o tipo MIME que a especificação recomenda.

Esqueleto do callback:

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

O JSON Feed não substitui o RSS nos podcasts nem nos leitores mais antigos. Trate-o como um segundo contrato para os seus próprios clientes e ferramentas internas. Discovery no HTML: type="application/feed+json".

Ao nível do canal, a especificação exige os campos version, title e items e, em cada item, id (string) e pelo menos um de content_html ou content_text. A versão 1.1 acrescentou o campo authors (array, vários autores por canal ou por item) e o campo language, e marcou o author singular como obsoleto. Mantenha o id de cada item estável: a especificação diz que o id não deve mudar quando o item é atualizado e que itens novos não devem reutilizar um id já usado. Por isso, no exemplo, o id é o guid e não a ligação permanente, que muda juntamente com o slug. content_text ou content_html: escolha um como fonte de verdade; enviar os dois com conteúdo divergente acaba numa pré-visualização inconsistente nos clientes.

#Como ativar WebSub no feed do WordPress

Sem WebSub, o leitor consulta o feed ao seu próprio ritmo, por isso um artigo novo chega aos subscritores com um atraso que depende do leitor. O WebSub, recomendação do W3C, inverte a direção: o editor indica um hub, os subscritores registam-se no hub e o hub distribui a atualização quando o editor assinala uma alteração.

Os requisitos do lado do editor são curtos. A especificação recomenda um cabeçalho Link com pelo menos um rel="hub" e exatamente um rel="self" e, para feeds XML, elementos link incorporados. O editor tem de informar os hubs indicados sobre a atualização do tópico, mas a especificação deixa em aberto o mecanismo dessa notificação. Numa nota, dá um exemplo comum em hubs públicos: um pedido POST com as chaves hub.mode=publish e hub.url a apontar para o recurso alterado.

O core do WordPress não implementa WebSub. No diretório de plugins existe o plugin “WebSub (FKA. PubSubHubbub)”, que acrescenta ambos os elementos. À mão, fica assim:

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

O rel="self" já está no template RSS2 (<atom:link rel="self">), por isso só falta acrescentar o rel="hub". Confirme o endereço do hub e o formato da notificação na documentação do hub escolhido, porque a especificação não os impõe. A verificação da subscrição (hub.challenge), a expiração após hub.lease_seconds e a assinatura X-Hub-Signature com segredo ficam a cargo do hub e do subscritor, não do WordPress.

#Como corrigir o erro de análise XML no feed do WordPress

O erro mais comum em templates e plugins próprios é um documento que o leitor rejeita com uma mensagem do tipo XML or text declaration not at start of entity. A declaração <?xml ... ?> tem de ser o primeiro carácter da resposta. Basta uma linha em branco ou um BOM escrito antes dela para o parser se recusar a processar o canal inteiro.

A causa está quase sempre num ficheiro PHP carregado em cada pedido: um espaço ou uma quebra de linha depois do ?> de fecho no functions.php ou num plugin, ou um ficheiro guardado em UTF-8 com BOM. A correção certa é remover o ?> de fecho dos ficheiros que contêm apenas PHP e guardar os ficheiros sem BOM. Para encontrar o culpado, execute curl -s https://dominio/feed/ | head -c 100 | xxd, veja o que aparece antes de <?xml e depois desative os plugins um a um.

Se não puder corrigir de imediato um plugin de terceiros, um buffer de saída que limpa o início da resposta do feed ajuda temporariamente:

declare(strict_types=1);

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

	ob_start(
		static function ( string $buffer ): string {
			// Remove o BOM e os espaços em branco antes da declaração XML.
			return ltrim( preg_replace( '/^\xEF\xBB\xBF/', '', $buffer ) );
		}
	);
}
add_action( 'template_redirect', 'wppoland_clean_feed_output', 1 );

É um remendo, não uma correção: o buffer não remove os caracteres escritos antes de o template_redirect ser executado. Equipas com testes de integração podem vigiar isto automaticamente. Os testes de feeds do próprio core carregam o template diretamente e suprimem os avisos sobre cabeçalhos; o mesmo padrão funciona em 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 ) );
	}
}

A asserção verifica o resultado em bruto, sem trim(), porque são precisamente os espaços em branco no início que constituem o erro.

#Como gerir feeds RSS num WordPress multilingue

Com WPML, Polylang ou prefixos de idioma próprios, o WordPress expõe normalmente vários endereços de feed. Erros típicos:

  • o feed principal mistura as versões de idioma, porque o pre_get_posts não filtra o idioma,
  • a CDN guarda o /feed/ em cache sem distinguir caminho ou cookie e serve o conteúdo de um idioma aos subscritores de outro,
  • os endereços no enclosure apontam para o domínio errado depois do mapeamento de domínios,
  • o elemento <language> do canal vem das definições do site, e não do idioma do conteúdo daquele feed concreto.

Mantenha um endpoint claro por idioma, filtre o idioma ou a taxonomia cedo na consulta e teste o feed com um leitor que envie de facto If-None-Match, e não apenas com um separador do navegador. Com várias marcas numa única instalação, um add_feed( 'marca-a', ... ) separado é mais limpo do que um parâmetro de consulta colado ao feed principal: os parâmetros fragmentam a cache na CDN, e o ETag do core depende apenas da data da última modificação, não dos parâmetros.

#O que verificar antes de publicar um feed RSS personalizado

Antes de enviar as alterações para produção:

  1. Abra o /feed/ no navegador e com curl -I: estado 200, Content-Type coerente.
  2. Depois de add_feed, ative o plugin / faça o flush das reescritas e verifique o novo URL.
  3. Remova os rel=alternate desnecessários, se estiver a desativar feeds.
  4. Verifique /author/.../feed/ quanto à enumeração.
  5. Se está a fazer um podcast, passe o XML pelo validador do diretório antes da submissão.
  6. Depois de publicar um artigo de teste, confirme que a CDN devolve XML atualizado dentro de um TTL aceitável.
  7. Confirme que os shortcodes e blocos dinâmicos no conteúdo do feed não deixam escapar HTML do painel de administração nem dados de sessão.
  8. Envie um segundo pedido com o cabeçalho If-None-Match definido com o ETag recebido e verifique se volta um 304.

O RSS no WordPress é uma camada de API, não uma relíquia. Controla a consulta, o template, a cache e a superfície de ataque tal como num endpoint REST. A diferença é que os clientes de feeds são mais antigos, menos tolerantes a XML mal formado e mais propensos a guardar a resposta em cache do seu lado.

Referências oficiais: Feeds (Developer Resources), add_feed e WordPress Feeds na documentação de administração. Antes de escrever mais um plugin “RSS Pack”, verifique qual destes três contratos falta realmente no seu tema.

Se precisa de alargar o feed com tipos de conteúdo próprios, campos meta ou um canal de podcast separado no âmbito do desenvolvimento para WordPress, comece com um único add_feed e um único template, sem um plugin monolítico “all feeds”.

Próximo passo

Transforme o artigo numa implementação real

Este bloco reforça a ligação interna e conduz o leitor para o passo seguinte mais útil dentro da arquitetura do site.

Quer implementar isto no seu site?

Se quer transformar o artigo em melhorias concretas, redesign ou num plano de implementação, posso fechar o escopo e executar.

Cluster relacionado

Explorar outros serviços WordPress e base de conhecimento

Reforce o seu negócio com suporte técnico profissional em áreas-chave do ecossistema WordPress.

FAQ do artigo

Perguntas frequentes

Respostas práticas para aplicar o tema na execução real.

SEO-readyGEO-readyAEO-ready3 Q&A
Porque é que os feeds RSS continuam relevantes?#
Porque ainda alimentam agregadores, importadores de newsletters, leitores RSS e diretórios de podcasts. Para estes clientes, o feed é o contrato de conteúdo mais simples que o WordPress oferece.
Como adicionar imagens ao feed RSS do WordPress?#
Através dos filtros the_content_feed e the_excerpt_rss, acrescentando a imagem destacada ao conteúdo de cada item, ou com um elemento media:content (Media RSS) adicionado no hook rss2_item.
Vale a pena atrasar o feed por motivos de SEO?#
Pode servir como margem temporal: atrasar os itens do feed principal em uma hora dá ao crawler do seu próprio domínio vantagem sobre scrapers e agregadores. Não substitui o rel=canonical nem o sitemap.

Precisa de FAQ adaptado ao setor e mercado? Criamos uma versão alinhada com os seus objetivos de negócio.

Fale connosco

Artigos Relacionados