Validador de feed RSS

Todas as regras do RSS 2.0, cada uma com sua referência.

Buscado direto da origem. Muitos servidores bloqueiam isso; se o seu bloquear, cole o conteúdo.
Entrada
AguardandoCole um documento para verificá-lo. A validação roda enquanto você digita.

Tudo roda nesta aba. Nada do que você colar é enviado, registrado ou transmitido para lugar nenhum. Abra o painel de rede e confira.

Cole seu feed acima, ou digite o endereço dele e clique em Buscar, e ele é verificado contra a especificação RSS 2.0. Cada achado nomeia o elemento, cita o valor e referencia a regra. Essa última parte importa: cerca de um terço das coisas pelas quais um feed leva bronca são convenções das quais os leitores dependem, e não violações da especificação, e aqui elas são rotuladas como tal.

Você provavelmente está aqui porque algo lá adiante rejeitou o feed. Um leitor mostra o canal mas nenhum episódio. Um serviço de RSS para e-mail recusa. Um assinante diz que todos os posts antigos reapareceram como novos. Cada caso tem uma lista curta de causas prováveis, e todas são verificadas.

Esta página existe porque a ferramenta canônica está se desfazendo em público. Verificado em setembro de 2026, o feedvalidator.org servia um certificado TLS expirado, então os navegadores se recusam a abri-lo, e o espelho dele no SourceForge tem copyright de 2002 a 2004 e ainda destaca o Atom 0.3. O validador do W3C funciona, mas é do lado do servidor: o seu feed vai para a máquina deles.

O que o RSS 2.0 realmente exige

Menos do que a maioria espera. A raiz é <rss> com version="2.0", contendo exatamente um <channel>. O channel precisa de title, link e description, e o link precisa ser uma URL absoluta. Os itens são mais soltos: pelo menos um entre title e description, e mais nada é obrigatório.

O que a especificação não permite é repetição. Dois elementos <title> num channel, ou dois <link> num item, são erros, e o segundo é reportado com a posição dele. Uma verificação estrutural pega um engano fácil: elementos <item> pendurados direto em <rss> em vez de dentro de <channel>. Eles fazem parsing, e nenhum leitor jamais vai mostrá-los.

  • Channel: title, link e description obrigatórios.
  • Item: pelo menos um entre title e description.
  • Elementos únicos não devem se repetir num channel nem num item.
  • Os elementos centrais do RSS ficam sem namespace, então <atom:link> não é um <link> de RSS.

Datas são RFC 822, não ISO 8601

O erro que mais parece certo em RSS. pubDate e lastBuildDate precisam ser RFC 822: Wed, 02 Oct 2024 13:00:00 GMT. O que a maioria dos frameworks emite por padrão é ISO 8601, 2024-10-02T13:00:00Z, que está certo para Atom e errado aqui. Um leitor que não consegue analisar um pubDate esconde o item ou o ordena no lugar errado.

Quatro coisas são verificadas. Se a data faz parsing. Se o dia da semana indicado bate com a data, já que a divergência é a digital de um formatador escrito à mão. Se o ano tem dois dígitos, o que o RSS permite e os agregadores leem errado. E se há fuso horário, já que a RFC 822 permite omitir e os leitores então chutam de formas diferentes.

Toda linguagem tem uma resposta de uma chamada só, usada em cada exemplo abaixo: toUTCString, format_datetime, RFC_1123_DATE_TIME, o especificador "r", DATE_RSS.

guid, e por que os assinantes veem seus posts antigos duas vezes

Um guid é a identidade de um item. Os leitores o usam para decidir se algo é novo ou já foi mostrado, então ele precisa ser estável e único. guids duplicados são erro: o leitor trata os dois itens como um e exibe apenas um deles.

A armadilha é o isPermaLink, cujo padrão é true, então um <guid>abc-123</guid> puro afirma que abc-123 é uma URL resolvível. Um identificador opaco precisa de isPermaLink="false" escrito explicitamente, e um guid que não é URL sem isso é erro. Um item sem guid é aviso: os leitores caem para casar por link ou por title.

A versão catastrófica é um plugin que regera os guids a cada build, normalmente a partir de algo volátil como o permalink. Todo assinante recebe o catálogo inteiro de uma vez.

"E comercial", enclosures, e as verificações que não dá para fazer

A maneira mais comum de um feed quebrar é um & solto dentro de uma URL, como em p?id=12&sort=asc. Em XML um "e comercial" inicia uma referência de entidade, então o parsing falha e os leitores rejeitam o documento inteiro em vez daquele item. Você recebe o erro de sintaxe com a posição, e uma nota de que nenhuma regra de RSS pôde rodar.

Um <enclosure> precisa de url, length e type. length é o tamanho em bytes como número inteiro, e 0 é aviso porque significa que o gerador não conseguiu dar stat no arquivo. Dois enclosures num item é aviso do Best Practices Profile, não da especificação.

O channel é checado quanto a um <atom:link rel="self">, que nem sequer é um elemento de RSS mas no qual os agregadores se apoiam. Três verificações aparecem como não executadas, já que um navegador não consegue fazê-las: se rel="self" bate com onde o feed é servido, e se as URLs e os length dos enclosures estão certos.

Verificando um feed em código

Vale rodar no seu pipeline de publicação, porque os modos de falha acima são silenciosos: o feed continua construindo e sendo servido, e você descobre por um assinante. Cada exemplo verifica a regra do item, o formato da data e o guid.

// Node 18 or later.  npm i @xmldom/xmldom
import { DOMParser } from '@xmldom/xmldom';

const xml = await (await fetch('https://example.com/feed.xml')).text();
const doc = new DOMParser().parseFromString(xml, 'text/xml');

const channel = doc.getElementsByTagName('channel').item(0);
for (const required of ['title', 'link', 'description']) {
  if (channel.getElementsByTagName(required).length === 0) {
    console.error('channel has no <' + required + '>');
  }
}

// RFC 822, not ISO 8601. Anything with a "T" separator is the wrong format.
const RFC822 = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}(:\d{2})? (GMT|UT|[A-Z]{3}|[+-]\d{4})$/;

const items = channel.getElementsByTagName('item');
for (let i = 0; i < items.length; i++) {
  const item = items.item(i);
  const text = (name) => {
    const el = item.getElementsByTagName(name).item(0);
    return el ? el.textContent.trim() : null;
  };

  if (!text('title') && !text('description')) {
    console.error('item ' + (i + 1) + ' has neither a title nor a description');
  }

  const pubDate = text('pubDate');
  if (pubDate && !RFC822.test(pubDate)) {
    console.error('item ' + (i + 1) + ' pubDate is not RFC 822: ' + pubDate);
  }

  const guid = item.getElementsByTagName('guid').item(0);
  if (guid && guid.getAttribute('isPermaLink') !== 'false'
      && !/^https?:\/\//.test(guid.textContent.trim())) {
    console.error('item ' + (i + 1) + ' guid defaults to isPermaLink="true" but is not a URL');
  }
}

// Producing a correct date is one call:
console.log(new Date().toUTCString());   // Wed, 02 Oct 2024 13:00:00 GMT
# pip install lxml requests
import sys
import requests
from email.utils import format_datetime, parsedate_to_datetime
from datetime import datetime, timezone
from lxml import etree

raw = requests.get('https://example.com/feed.xml', timeout=30).content

# resolve_entities=False and no_network=True are the safe form.
parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
root = etree.fromstring(raw, parser)

channel = root.find('channel')
if channel is None:
    sys.exit('No <channel> element')

for required in ('title', 'link', 'description'):
    if channel.find(required) is None:
        print('channel has no <%s>' % required)

for i, item in enumerate(channel.findall('item'), start=1):
    if item.find('title') is None and item.find('description') is None:
        print('item %d has neither a title nor a description' % i)

    pub = item.findtext('pubDate')
    if pub:
        try:
            # parsedate_to_datetime is the RFC 2822 parser, and it rejects
            # ISO 8601 outright, which is exactly what you want here.
            parsedate_to_datetime(pub)
        except (TypeError, ValueError):
            print('item %d pubDate is not RFC 822: %s' % (i, pub))

    guid = item.find('guid')
    if guid is not None and guid.get('isPermaLink', 'true') == 'true':
        if not (guid.text or '').startswith(('http://', 'https://')):
            print('item %d guid is treated as a permalink but is not a URL' % i)

# Producing a correct date:
print(format_datetime(datetime.now(timezone.utc)))
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.ZonedDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

byte[] bytes = HttpClient.newHttpClient()
    .send(HttpRequest.newBuilder(URI.create("https://example.com/feed.xml")).build(),
          HttpResponse.BodyHandlers.ofByteArray())
    .body();

DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
f.setNamespaceAware(true);
f.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
f.setXIncludeAware(false);

Document doc = f.newDocumentBuilder().parse(new ByteArrayInputStream(bytes));
Element channel = (Element) doc.getElementsByTagName("channel").item(0);

for (String required : new String[] { "title", "link", "description" }) {
    if (channel.getElementsByTagName(required).getLength() == 0) {
        System.err.println("channel has no <" + required + ">");
    }
}

NodeList items = channel.getElementsByTagName("item");
for (int i = 0; i < items.getLength(); i++) {
    Element item = (Element) items.item(i);
    boolean hasTitle = item.getElementsByTagName("title").getLength() > 0;
    boolean hasDesc = item.getElementsByTagName("description").getLength() > 0;
    if (!hasTitle && !hasDesc) {
        System.err.println("item " + (i + 1) + " has neither a title nor a description");
    }

    NodeList pub = item.getElementsByTagName("pubDate");
    if (pub.getLength() > 0) {
        String v = pub.item(0).getTextContent().trim();
        try {
            // RFC_1123_DATE_TIME is the RFC 822 profile RSS wants, and it
            // refuses ISO 8601, so a wrong-format date fails loudly here.
            DateTimeFormatter.RFC_1123_DATE_TIME.parse(v);
        } catch (DateTimeParseException e) {
            System.err.println("item " + (i + 1) + " pubDate is not RFC 822: " + v);
        }
    }
}

// Producing a correct date:
System.out.println(DateTimeFormatter.RFC_1123_DATE_TIME
    .format(ZonedDateTime.now(ZoneOffset.UTC)));
using System.Globalization;
using System.Xml;
using System.Xml.Linq;

var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,  // external entities are never fetched
    XmlResolver = null,
};

var bytes = await new HttpClient().GetByteArrayAsync("https://example.com/feed.xml");
using var stream = new MemoryStream(bytes);
using var reader = XmlReader.Create(stream, settings);
var doc = XDocument.Load(reader);

// RSS core elements are in no namespace, so plain element names are correct here.
var channel = doc.Root?.Element("channel");
if (channel is null) throw new InvalidOperationException("No <channel> element");

foreach (var required in new[] { "title", "link", "description" })
{
    if (channel.Element(required) is null)
        Console.Error.WriteLine("channel has no <" + required + ">");
}

int n = 0;
foreach (var item in channel.Elements("item"))
{
    n++;
    if (item.Element("title") is null && item.Element("description") is null)
        Console.Error.WriteLine("item " + n + " has neither a title nor a description");

    var pub = (string?)item.Element("pubDate");
    if (pub is not null)
    {
        // .NET has no dedicated RFC 822 parser. TryParse handles the usual
        // spellings but also accepts ISO 8601, which RSS forbids, so reject
        // anything carrying a "T" separator before parsing.
        if (pub.Contains('T') ||
            !DateTimeOffset.TryParse(pub, CultureInfo.InvariantCulture,
                                     DateTimeStyles.None, out _))
        {
            Console.Error.WriteLine("item " + n + " pubDate is not RFC 822: " + pub);
        }
    }

    var guid = item.Element("guid");
    if (guid is not null && (string?)guid.Attribute("isPermaLink") != "false"
        && !guid.Value.StartsWith("http", StringComparison.Ordinal))
    {
        Console.Error.WriteLine("item " + n + " guid is a permalink by default but is not a URL");
    }
}

// Producing a correct date: "r" is the RFC 1123 pattern RSS accepts.
Console.WriteLine(DateTimeOffset.UtcNow.ToString("r", CultureInfo.InvariantCulture));
<?php
// LIBXML_NONET stops libxml fetching anything the document references.
libxml_use_internal_errors(true);

$raw = file_get_contents('https://example.com/feed.xml');
$feed = simplexml_load_string($raw, 'SimpleXMLElement', LIBXML_NONET);

if ($feed === false) {
    foreach (libxml_get_errors() as $e) {
        fprintf(STDERR, "XML error at line %d: %s", $e->line, trim($e->message) . "\n");
    }
    exit(1);
}

foreach (['title', 'link', 'description'] as $required) {
    if (!isset($feed->channel->{$required})) {
        fprintf(STDERR, "channel has no <%s>\n", $required);
    }
}

$n = 0;
foreach ($feed->channel->item as $item) {
    $n++;

    if (!isset($item->title) && !isset($item->description)) {
        fprintf(STDERR, "item %d has neither a title nor a description\n", $n);
    }

    if (isset($item->pubDate)) {
        $raw_date = (string) $item->pubDate;
        // DATE_RSS expects a numeric offset; the second pattern covers "GMT".
        $parsed = DateTimeImmutable::createFromFormat(DATE_RSS, $raw_date)
            ?: DateTimeImmutable::createFromFormat('D, d M Y H:i:s T', $raw_date);
        if ($parsed === false) {
            fprintf(STDERR, "item %d pubDate is not RFC 822: %s\n", $n, $raw_date);
        }
    }

    $guid = $item->guid;
    if ($guid !== null && (string) ($guid['isPermaLink'] ?? 'true') !== 'false'
        && !preg_match('#^https?://#', (string) $guid)) {
        fprintf(STDERR, "item %d guid defaults to isPermaLink=true but is not a URL\n", $n);
    }
}

// Producing a correct date: DATE_RSS is RFC 822.
echo (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format(DATE_RSS), "\n";

Todo exemplo desliga a resolução de entidades externas, porque um feed é um documento que você busca de outro lugar e não escreveu. Em Java e PHP o comportamento inseguro é o padrão.

Perguntas frequentes

Meu feed funciona num leitor e noutro não. O que está errado?

Normalmente uma data, um guid ou a codificação. Os leitores diferem enormemente no que perdoam, então um feed que o Feedly renderiza feliz pode ser rejeitado por um cliente mais rígido.

Vá por partes. Verifique se o documento é sequer XML bem formado, já que um "e comercial" solto quebra tudo dali para a frente. Depois verifique o pubDate de cada item, porque uma data que não faz parsing some com itens ou os ordena de forma estranha. Depois verifique a unicidade dos guids. Se o feed está limpo aqui e um leitor ainda se comporta mal, a causa é o transporte: Content-Type errado, cadeia de redirecionamentos, ou cache.

O feedvalidator.org ainda é a ferramenta que eu deveria usar?

Foi a resposta canônica por vinte anos, mas em setembro de 2026 ele serve um certificado TLS expirado, que os navegadores bloqueiam. O espelho dele no SourceForge tem copyright de 2002 a 2004 e ainda anuncia o Atom 0.3, que a RFC 4287 substituiu em 2005.

O Feed Validation Service do W3C é mantido e o catálogo de erros dele é minucioso. Os limites são idade e arquitetura: mensagens secas, e o seu feed enviado ao servidor deles. Esta página reproduz as verificações daquele catálogo que correspondem a quebras reais e as roda localmente.

Que formato de data o RSS precisa?

RFC 822: Wed, 02 Oct 2024 13:00:00 GMT. Um deslocamento numérico como +0000 é igualmente válido no lugar do nome do fuso. O dia da semana é opcional, mas se você incluir, precisa ser o certo.

ISO 8601, ou seja 2024-10-02T13:00:00Z, não é válido em RSS. É válido em Atom, e é por isso que tantos geradores o emitem: alguém copiou um auxiliar de datas de um template de Atom. Evite anos de dois dígitos e sempre inclua um fuso horário.

Por que todos os meus posts antigos reapareceram como novos para os assinantes?

Algo mudou o guid de todos os itens. Um leitor guarda a lista de guids que já mostrou, e tudo que não está nela é novo. Mude todos de uma vez e cada assinante recebe o arquivo de novo, o que num podcast significa cada episódio baixando em cada aparelho.

A causa é um guid derivado de algo impermanente: um construído a partir do permalink quebra quando você migra para https, um construído a partir do título quebra quando você corrige um erro de digitação. Use uma chave de banco, um UUID ou uma tag URI, com isPermaLink="false".

Meu feed é enviado para um servidor?

Não. O analisador e todas as regras rodam nesta aba como JavaScript. Não há back-end para onde mandar nada nem analytics com acesso ao editor. Abra o painel de rede enquanto valida e veja ele continuar vazio.

Parece pouco importante para um formato tão público quanto o RSS, até você considerar que feeds as pessoas colam num validador: programas que não foram lançados, feeds privados de podcast cujas URLs de enclosure carregam um token por assinante, feeds de homologação cheios de rascunhos.

Isto valida feeds de podcast e tags do iTunes?

Valida a camada RSS 2.0, que é onde começa boa parte das rejeições de podcast: um pubDate malformado, um guid duplicado, um enclosure sem length ou type. Essas regras importam especialmente aqui, já que o enclosure é o episódio.

O que não é verificado é o namespace itunes: itunes:image e as regras de dimensão dele, itunes:category, itunes:explicit, itunes:duration. Eles ficam intocados a não ser que o prefixo esteja sem declaração. Uma regra da Apple vale repetir: cada episódio precisa de um identificador globalmente único que nunca mude, que é a verificação de guid acima.

Ferramentas relacionadas

Leitura complementar

Erros que isto resolve