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:
- Discovery: o core escreve
<link rel="alternate" type="application/rss+xml" ...>nowp_headatravés das funçõesfeed_linksefeed_links_extra, desde que o tema declareadd_theme_support( 'automatic-feed-links' ). O leitor e o crawler usam o que veem no HTML, não o que “deveria estar lá”. - Âmbito da consulta: o feed principal inclui artigos (
post). CPT, páginas e estados personalizados ficam de fora até alterar opre_get_posts. - 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 deget_the_content_feed(). Os filtrosthe_content_feedethe_excerpt_rssatuam 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çãohourlye1, alteráveis com os filtrosrss_update_perioderss_update_frequency,- em cada item,
<dc:creator>, categorias,<pubDate>no fuso+0000e<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-Typeantes 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, estadopublish, 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-Controlde forma consciente. Um feed público commax-age=300costuma 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_sete um TTL curto, e limpe-a emtransition_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 oContent-Typeerrado. Exclua os caminhosfeeddas regras de cache ou force o bypass parais_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 XMLUm 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:
- Remova as ligações de discovery:
remove_action( 'wp_head', 'feed_links', 2 )eremove_action( 'wp_head', 'feed_links_extra', 3 ), com as prioridades com que o core as regista. - Decida se os URLs antigos recebem um 301 para conteúdo HTML, um 410 vazio, ou se continuam a servir XML.
- 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
nicenamelimpo, 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 exemploitunes:author,itunes:category,itunes:image) elanguage, - 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_postsnã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
enclosureapontam 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:
- Abra o
/feed/no navegador e comcurl -I: estado 200,Content-Typecoerente. - Depois de
add_feed, ative o plugin / faça o flush das reescritas e verifique o novo URL. - Remova os
rel=alternatedesnecessários, se estiver a desativar feeds. - Verifique
/author/.../feed/quanto à enumeração. - Se está a fazer um podcast, passe o XML pelo validador do diretório antes da submissão.
- Depois de publicar um artigo de teste, confirme que a CDN devolve XML atualizado dentro de um TTL aceitável.
- 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.
- Envie um segundo pedido com o cabeçalho
If-None-Matchdefinido com oETagrecebido 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”.







