Validador de feed Atom
RFC 4287 conferida a sério, herança de author inclusive.
Tudo roda nesta aba. Nada do que você colar é enviado, registrado ou transmitido para lugar nenhum. Abra o painel de rede e confira.
Cole um feed Atom acima, ou busque-o por URL, e ele é verificado em relação ao RFC 4287. Cada achado cita a seção de onde vem. Documentos de entrada Atom avulsos, em que a raiz é <entry> em vez de <feed>, são reconhecidos e verificados pelas regras de entrada.
Atom é mais rigoroso que RSS, e é justamente esse o seu propósito: quase tudo o que o RSS deixou à interpretação, o Atom decidiu. Exatamente um id, um title e um updated no feed e em cada entrada. Um único formato de data, sem variantes. Cada construção de texto declara se é texto, HTML ou XHTML.
Há aqui uma regra implementada como deve ser e que a maioria das ferramentas pula: a herança de author. Uma entrada não precisa de autor próprio se tiver um <atom:source> que carregue um, ou se o feed tiver um. Verificar apenas entry/author produz uma página de erros falsos num feed válido; não verificar nada deixa passar uma falha real.
Por que o Atom existe, e em que difere do RSS
O RSS 2.0 foi congelado em 2002, e suas ambiguidades junto com ele. A maior era <description>: a especificação nunca disse se contém texto puro ou HTML, então cada leitor adivinhava. A identidade do item era opcional. As datas seguiam o RFC 822, que permite anos de dois dígitos e nenhum fuso horário.
O Atom foi escrito na IETF como resposta e publicado como RFC 4287 em dezembro de 2005. O texto carrega um type explícito. Cada feed e cada entrada têm um id obrigatório e permanente. As datas são RFC 3339 com fuso obrigatório. Tudo vive num único espaço de nomes, de modo que uma extensão não pode colidir com o vocabulário do núcleo.
- RSS: description pode ser texto ou HTML, ninguém sabe qual. Atom: o type é declarado.
- RSS: guid opcional e muitas vezes instável. Atom: id obrigatório, IRI absoluta, permanente.
- RSS: datas RFC 822, fuso opcional. Atom: RFC 3339, fuso obrigatório.
O que um feed e suas entradas precisam conter
Exatamente um <id>, um <title> e um <updated> no feed, e esses mesmos três em cada entrada. Não um ou mais: exatamente um, portanto uma duplicata também é erro. A falha mais comum é um feed com título e entradas, mas sem id.
Uma entrada sem <content> precisa carregar ao menos um <link rel="alternate">: ela tem de oferecer ao leitor ou a coisa em si, ou um caminho até ela. Se <content> tiver um atributo src, o elemento deve estar vazio e a entrada precisa de um <summary>, o que também vale para conteúdo em base64.
Todo <link> precisa de href, e nada pode carregar dois links rel="alternate" com o mesmo type e o mesmo hreflang, pois um leitor não teria como escolher entre eles. Um rel ausente significa alternate. Um feed também deveria carregar um <link rel="self">, que o RFC 4287 formula como SHOULD, portanto é um aviso rotulado.
A regra de herança de author
O RFC 4287 exige que se possa resolver um autor para cada entrada, mas não que o elemento esteja na entrada. A regra é uma cadeia de recurso: o <author> da própria entrada, ou um <author> dentro do seu <atom:source>, ou um <author> no <feed>. O caso do source existe para agregadores que republicam entradas recolhidas noutro lugar.
Quase nenhum validador gratuito implementa os três passos. Os que só verificam entry/author relatam erro em cada entrada de um feed de blog de autor único perfeitamente válido, que é a forma mais comum de feed Atom que existe. Esta ferramenta percorre a cadeia e só relata erro quando os três falham.
Um documento de entrada Atom avulso não tem feed do qual herdar, então precisa carregar o próprio autor. Toda construção de pessoa precisa ainda de um <name>: um <author> só com endereço de e-mail é erro pela seção 3.2.1.
Datas e ids, exatamente como o RFC os escreve
As marcas de tempo do Atom são RFC 3339, e a seção 3.3 acrescenta dois requisitos. O separador deve ser um T maiúsculo, e sem deslocamento numérico o fuso deve ser um Z maiúsculo. Portanto 2026-03-14T09:30:00Z é válido e 2026-03-14t09:30:00z não é, embora qualquer biblioteca de datas o analisasse. As minúsculas têm mensagem própria. Uma data isolada também é inválida.
Um id precisa ser uma IRI absoluta, logo precisa de esquema: https:, tag: e urn: servem; um caminho relativo ou uma cadeia solta, não. Ids duplicados entre entradas são erro, já que os leitores desduplicam pelo id.
Ids que diferem apenas em maiúsculas, numa barra final ou numa porta padrão geram aviso, porque ids são comparados caractere a caractere: /p/1 e /p/1/ são duas entradas. O esquema tag, o RFC 4151, evita isso, e tag:example.com,2026:post-4192 sobrevive a uma mudança de domínio.
Produzir e verificar Atom em código
As quatro regras que valem a automação são exatamente as que um esquema não consegue expressar: as maiúsculas do RFC 3339, a unicidade dos ids, a herança de author e a alternativa content ou link alternate. Cada exemplo verifica essas, na forma segura de análise.
// Node 18 or later. npm i @xmldom/xmldom
import { DOMParser } from '@xmldom/xmldom';
const ATOM = 'http://www.w3.org/2005/Atom';
// RFC 3339 as RFC 4287 section 3.3 requires it: uppercase T, uppercase Z.
const RFC3339 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
const xml = await (await fetch('https://example.com/atom.xml')).text();
const doc = new DOMParser().parseFromString(xml, 'text/xml');
const feed = doc.documentElement;
const kids = (el, name) => Array.from(el.getElementsByTagNameNS(ATOM, name))
.filter((n) => n.parentNode === el);
for (const required of ['id', 'title', 'updated']) {
if (kids(feed, required).length !== 1) {
console.error('feed must have exactly one <' + required + '>');
}
}
const feedHasAuthor = kids(feed, 'author').length > 0;
const seen = new Map();
for (const [i, entry] of kids(feed, 'entry').entries()) {
const n = i + 1;
const updated = kids(entry, 'updated')[0];
if (updated && !RFC3339.test(updated.textContent.trim())) {
console.error('entry ' + n + ' updated is not RFC 3339: ' + updated.textContent.trim());
}
const id = kids(entry, 'id')[0];
const value = id ? id.textContent.trim() : '';
if (!/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value)) {
console.error('entry ' + n + ' id is not an absolute IRI: ' + value);
} else if (seen.has(value)) {
console.error('entry ' + n + ' repeats the id of entry ' + seen.get(value));
} else {
seen.set(value, n);
}
// The inheritance chain: entry/author, else entry/source/author, else feed/author.
const source = kids(entry, 'source')[0];
const hasAuthor = kids(entry, 'author').length > 0
|| (source ? kids(source, 'author').length > 0 : false)
|| feedHasAuthor;
if (!hasAuthor) console.error('entry ' + n + ' has no resolvable author');
const hasAlternate = kids(entry, 'link')
.some((l) => (l.getAttribute('rel') || 'alternate') === 'alternate');
if (kids(entry, 'content').length === 0 && !hasAlternate) {
console.error('entry ' + n + ' has neither <content> nor a <link rel="alternate">');
}
}
// Producing a correct timestamp:
console.log(new Date().toISOString()); // 2026-03-14T09:30:00.000Z# pip install lxml requests
import re
import sys
import requests
from datetime import datetime, timezone
from lxml import etree
ATOM = 'http://www.w3.org/2005/Atom'
NS = {'a': ATOM}
# datetime.fromisoformat is too permissive for this: it accepts a lowercase
# separator, which RFC 4287 section 3.3 forbids. Check the shape directly.
RFC3339 = re.compile(r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$')
IRI = re.compile(r'^[A-Za-z][A-Za-z0-9+.\-]*:')
raw = requests.get('https://example.com/atom.xml', timeout=30).content
parser = etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False)
feed = etree.fromstring(raw, parser)
for required in ('id', 'title', 'updated'):
if len(feed.findall('a:%s' % required, NS)) != 1:
print('feed must have exactly one <%s>' % required)
feed_has_author = len(feed.findall('a:author', NS)) > 0
seen = {}
for n, entry in enumerate(feed.findall('a:entry', NS), start=1):
updated = entry.findtext('a:updated', namespaces=NS) or ''
if not RFC3339.match(updated.strip()):
print('entry %d updated is not RFC 3339: %s' % (n, updated))
ident = (entry.findtext('a:id', namespaces=NS) or '').strip()
if not IRI.match(ident):
print('entry %d id is not an absolute IRI: %s' % (n, ident))
elif ident in seen:
print('entry %d repeats the id of entry %d' % (n, seen[ident]))
else:
seen[ident] = n
has_author = (len(entry.findall('a:author', NS)) > 0
or len(entry.findall('a:source/a:author', NS)) > 0
or feed_has_author)
if not has_author:
print('entry %d has no resolvable author' % n)
alternates = [l for l in entry.findall('a:link', NS)
if l.get('rel', 'alternate') == 'alternate']
if entry.find('a:content', NS) is None and not alternates:
print('entry %d has neither <content> nor a <link rel="alternate">' % n)
# Producing a correct timestamp. Both spellings below are valid RFC 3339.
print(datetime.now(timezone.utc).isoformat(timespec='seconds')) # ...+00:00
print(datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')) # ...Zimport 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.Instant;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.HashMap;
import java.util.Map;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
final String ATOM = "http://www.w3.org/2005/Atom";
byte[] bytes = HttpClient.newHttpClient()
.send(HttpRequest.newBuilder(URI.create("https://example.com/atom.xml")).build(),
HttpResponse.BodyHandlers.ofByteArray())
.body();
DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
f.setNamespaceAware(true); // without this, every Atom lookup below finds nothing
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 feed = doc.getDocumentElement();
boolean feedHasAuthor = childCount(feed, ATOM, "author") > 0;
Map<String, Integer> seen = new HashMap<>();
NodeList entries = feed.getElementsByTagNameNS(ATOM, "entry");
for (int i = 0; i < entries.getLength(); i++) {
Element entry = (Element) entries.item(i);
int n = i + 1;
String updated = firstText(entry, ATOM, "updated");
// java.time matches literals case-sensitively, but check explicitly so the
// lowercase case gets its own message: RFC 4287 requires "T" and "Z".
if (updated.indexOf('t') >= 0 || updated.indexOf('z') >= 0) {
System.err.println("entry " + n + " updated uses a lowercase T or Z: " + updated);
} else {
try {
OffsetDateTime.parse(updated);
} catch (DateTimeParseException e) {
System.err.println("entry " + n + " updated is not RFC 3339: " + updated);
}
}
String id = firstText(entry, ATOM, "id");
if (!id.matches("[A-Za-z][A-Za-z0-9+.\\-]*:.+")) {
System.err.println("entry " + n + " id is not an absolute IRI: " + id);
} else if (seen.containsKey(id)) {
System.err.println("entry " + n + " repeats the id of entry " + seen.get(id));
} else {
seen.put(id, n);
}
// entry/author, else entry/source/author, else feed/author.
NodeList sources = entry.getElementsByTagNameNS(ATOM, "source");
boolean sourceAuthor = sources.getLength() > 0
&& childCount((Element) sources.item(0), ATOM, "author") > 0;
if (childCount(entry, ATOM, "author") == 0 && !sourceAuthor && !feedHasAuthor) {
System.err.println("entry " + n + " has no resolvable author");
}
}
// Producing a correct timestamp:
System.out.println(DateTimeFormatter.ISO_INSTANT.format(Instant.now()));using System.Globalization;
using System.Xml;
using System.Xml.Linq;
XNamespace atom = "http://www.w3.org/2005/Atom";
// RFC 4287 section 3.3: uppercase T, and uppercase Z when there is no offset.
// The literals are quoted so they are matched exactly rather than as specifiers.
string[] rfc3339 =
{
"yyyy-MM-dd'T'HH:mm:ssK",
"yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK",
};
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit, // external entities are never fetched
XmlResolver = null,
};
var bytes = await new HttpClient().GetByteArrayAsync("https://example.com/atom.xml");
using var stream = new MemoryStream(bytes);
using var reader = XmlReader.Create(stream, settings);
var feed = XDocument.Load(reader).Root!;
foreach (var required in new[] { "id", "title", "updated" })
{
if (feed.Elements(atom + required).Count() != 1)
Console.Error.WriteLine("feed must have exactly one <" + required + ">");
}
bool feedHasAuthor = feed.Elements(atom + "author").Any();
var seen = new Dictionary<string, int>(StringComparer.Ordinal);
int n = 0;
foreach (var entry in feed.Elements(atom + "entry"))
{
n++;
var updated = ((string?)entry.Element(atom + "updated") ?? string.Empty).Trim();
if (!DateTimeOffset.TryParseExact(updated, rfc3339, CultureInfo.InvariantCulture,
DateTimeStyles.None, out _))
{
Console.Error.WriteLine("entry " + n + " updated is not RFC 3339: " + updated);
}
var id = ((string?)entry.Element(atom + "id") ?? string.Empty).Trim();
if (!Uri.IsWellFormedUriString(id, UriKind.Absolute))
Console.Error.WriteLine("entry " + n + " id is not an absolute IRI: " + id);
else if (seen.TryGetValue(id, out int first))
Console.Error.WriteLine("entry " + n + " repeats the id of entry " + first);
else
seen[id] = n;
// entry/author, else entry/source/author, else feed/author.
bool hasAuthor = entry.Elements(atom + "author").Any()
|| entry.Elements(atom + "source").Elements(atom + "author").Any()
|| feedHasAuthor;
if (!hasAuthor) Console.Error.WriteLine("entry " + n + " has no resolvable author");
bool hasAlternate = entry.Elements(atom + "link")
.Any(l => ((string?)l.Attribute("rel") ?? "alternate") == "alternate");
if (entry.Element(atom + "content") is null && !hasAlternate)
Console.Error.WriteLine("entry " + n + " has no content and no alternate link");
}
// Producing a correct timestamp: "o" on a UTC DateTime ends in Z.
Console.WriteLine(DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture));# RFC 4287 Appendix B carries a RELAX NG Compact schema for Atom. trang
# converts it to the XML syntax xmllint understands.
trang atom.rnc atom.rng
xmllint --noout --nonet --relaxng atom.rng feed.xml
# That schema is informative and cannot express every rule in the RFC. Author
# inheritance and id uniqueness are two of the rules it cannot state, so check
# them separately. Entries carrying no author of their own:
xmlstarlet sel -N a=http://www.w3.org/2005/Atom \
-t -v 'count(//a:entry[not(a:author) and not(a:source/a:author)])' -n feed.xml
# If that is not zero, the <feed> element itself needs an <author>.
# Duplicate entry ids. Any output at all is a bug:
xmlstarlet sel -N a=http://www.w3.org/2005/Atom \
-t -m '//a:entry/a:id' -v . -n feed.xml | sort | uniq -d
# Timestamps with a lowercase t or z, which RFC 4287 section 3.3 forbids:
grep -n -E '<(updated|published)>[^<]*[tz]' feed.xmlVale a pena rodar o esquema RELAX NG do RFC 4287, mas conheça seus limites. Ele é informativo e não normativo, e uma gramática não consegue expressar restrições que atravessam vários elementos: a herança de author, a unicidade dos ids e a regra do summary têm todas de ser verificadas em código.
Perguntas frequentes
Qual é a diferença entre RSS e Atom?
O Atom é o formato posterior e o mais rigoroso. É um padrão da IETF, o RFC 4287, publicado em dezembro de 2005; o RSS 2.0 foi congelado em 2002 e é mantido como documento de especificação, não como padrão.
As diferenças que importam giram em torno da ambiguidade. No RSS ninguém sabe dizer se uma <description> contém texto ou HTML; no Atom cada construção de texto declara o seu tipo. O RSS torna a identidade do item opcional e permite várias escritas de data; o Atom exige um id permanente e um único formato de data com fuso.
Por que o validador diz que minha entrada não tem autor se o feed tem?
Não deveria, e se disser é porque o elemento author provavelmente não está onde você pensa. A regra implementada aqui é a cadeia do RFC 4287: o <author> da própria entrada, ou um dentro do seu <atom:source>, ou um no elemento <feed>.
A causa habitual da surpresa é o espaço de nomes. Um <author> que não esteja em http://www.w3.org/2005/Atom, um dc:creator do Dublin Core por exemplo, não é um author do Atom e não satisfaz a regra. A outra é a colocação: um <author> dentro da primeira <entry> cobre apenas aquela entrada.
Por que 2026-03-14 não é uma data Atom válida?
Porque a seção 3.3 do RFC 4287 exige uma data-hora RFC 3339 completa, não uma data. As marcas de tempo do Atom precisam da hora e de um fuso: 2026-03-14T09:30:00Z, ou 2026-03-14T09:30:00+05:30 para um deslocamento.
Outros dois detalhes são MUST explícitos. O separador deve ser um T maiúsculo, e sem deslocamento numérico o fuso deve ser um Z maiúsculo. Um t ou um z minúsculo é analisado sem reclamação por qualquer biblioteca de datas e continua inválido, e por isso tem mensagem de erro própria.
O que devo usar como id?
Algo globalmente único que você nunca precise mudar. A especificação é taxativa: um id não deve mudar quando a entrada é movida ou republicada, porque os leitores comparam ids caractere a caractere para decidir o que é novo.
O esquema de tag URI, o RFC 4151, foi desenhado para isso. tag:example.com,2026:post-4192 é único, não se espera que resolva, e não prende a entrada ao lugar onde ela vive hoje. Uma URL de página funciona, mas compromete: acrescentar uma barra final ou migrar para https muda a identidade de cada entrada, e os assinantes recebem o arquivo todo de novo.
Meu feed é enviado para algum lugar quando eu o valido aqui?
Não. O analisador e todas as regras do RFC 4287 são JavaScript rodando nesta aba. Não há componente de servidor nem analytics com acesso ao editor. Abra o painel de rede e valide algo: os recursos da página carregam uma vez, e depois nada.
Os feeds que as pessoas colam num validador são justamente os que ainda não funcionam: um site não lançado, um feed cujos links carregam tokens de acesso, um feed de cliente sob acordo de confidencialidade. O serviço do W3C é do lado do servidor, então tudo o que é verificado lá é transmitido. Aqui, Buscar vai do seu navegador direto ao host que você nomeou.
Ele consegue verificar um feed Atom 0.3?
Não, e diz isso com clareza. O Atom 0.3 usava o espaço de nomes http://purl.org/atom/ns#, e um feed que o declare é relatado como erro nomeando a versão, junto com o espaço de nomes 1.0 de que você precisa em seu lugar.
O Atom 0.3 era um rascunho pré-padrão que o RFC 4287 substituiu em 2005, e pelo caminho nomes de elementos mudaram: o 0.3 tem <modified> e <issued> onde o 1.0 tem <updated> e <published>, e <tagline> virou <subtitle>. Mudar só o espaço de nomes produz um documento inválido de um jeito novo, então a mensagem manda revisar também os nomes de elemento.