Atom-Feed-Validator
RFC 4287 richtig geprüft, inklusive author-Vererbung.
Alles läuft in diesem Tab. Nichts, was Sie einfügen, wird hochgeladen, protokolliert oder irgendwohin gesendet. Öffnen Sie Ihr Netzwerkpanel und prüfen Sie es.
Fügen Sie oben einen Atom-Feed ein oder rufen Sie ihn per URL ab: Er wird gegen RFC 4287 geprüft. Jeder Befund nennt den Abschnitt, aus dem er stammt. Eigenständige Atom-Entry-Dokumente, deren Wurzel <entry> statt <feed> ist, werden erkannt und nach den Entry-Regeln geprüft.
Atom ist strenger als RSS, und darin liegt sein Sinn: Fast alles, was RSS der Auslegung überließ, hat Atom entschieden. Genau ein id, ein title und ein updated am Feed und an jedem Entry. Ein einziges Datumsformat ohne Varianten. Jede Textkonstruktion erklärt, ob sie Text, HTML oder XHTML ist.
Eine Regel ist hier sauber umgesetzt, die die meisten Werkzeuge auslassen: die Vererbung von author. Ein Entry braucht keinen eigenen Autor, wenn es ein <atom:source> mit einem trägt oder der Feed einen hat. Nur entry/author zu prüfen erzeugt eine Seite voll falscher Fehler auf einem gültigen Feed; gar nichts zu prüfen übersieht ein echtes Versäumnis.
Warum es Atom gibt, und wie es sich von RSS unterscheidet
RSS 2.0 wurde 2002 eingefroren, und seine Mehrdeutigkeiten gleich mit. Die größte war <description>: Die Spezifikation sagte nie, ob dort reiner Text oder HTML steht, also riet jeder Reader. Die Identität eines Items war optional. Datumsangaben folgten RFC 822, das zweistellige Jahre und gar keine Zeitzone erlaubt.
Atom wurde als Antwort darauf bei der IETF geschrieben und im Dezember 2005 als RFC 4287 veröffentlicht. Text trägt einen expliziten type. Jeder Feed und jedes Entry hat eine verpflichtende, dauerhafte id. Datumsangaben sind RFC 3339 mit Pflichtzone. Alles liegt in einem Namensraum, sodass eine Erweiterung nicht mit dem Kernvokabular kollidieren kann.
- RSS: description kann Text oder HTML sein, niemand weiß welches. Atom: type angegeben.
- RSS: guid optional und oft instabil. Atom: id verpflichtend, absolute IRI, dauerhaft.
- RSS: RFC-822-Datumsangaben, Zone optional. Atom: RFC 3339, Zone verpflichtend.
Was ein Feed und seine Entries enthalten müssen
Genau eine <id>, ein <title> und ein <updated> am Feed, und dieselben drei an jedem Entry. Nicht eins oder mehr: genau eins, ein Duplikat ist also ebenfalls ein Fehler. Der häufigste Ausfall ist ein Feed mit Titel und Einträgen, aber ohne id.
Ein Entry ohne <content> muss mindestens einen <link rel="alternate"> tragen: Es muss dem Leser entweder die Sache selbst oder einen Weg dorthin anbieten. Hat <content> ein src-Attribut, muss das Element leer sein und das Entry braucht ein <summary>, was auch für base64-Inhalte gilt.
Jeder <link> braucht ein href, und nichts darf zwei rel="alternate"-Links mit gleichem type und hreflang tragen, weil ein Reader nicht zwischen ihnen wählen könnte. Ein fehlendes rel bedeutet alternate. Ein Feed sollte außerdem einen <link rel="self"> tragen, was RFC 4287 als SHOULD formuliert – also eine gekennzeichnete Warnung.
Die Vererbungsregel für author
RFC 4287 verlangt, dass sich für jedes Entry ein Autor auflösen lässt, nicht aber, dass das Element am Entry steht. Die Regel ist eine Rückfallkette: der eigene <author> des Entrys, oder ein <author> in seinem <atom:source>, oder ein <author> am <feed>. Der source-Fall existiert für Aggregatoren, die anderswo eingesammelte Entries erneut veröffentlichen.
Fast kein kostenloser Validator setzt alle drei Stufen um. Diejenigen, die nur entry/author prüfen, melden an jedem Entry eines gültigen Ein-Autor-Blogfeeds einen Fehler – der verbreitetsten Form eines Atom-Feeds überhaupt. Dieses Werkzeug geht die Kette ab und meldet nur dann einen Fehler, wenn alle drei Stufen scheitern.
Ein eigenständiges Atom-Entry-Dokument hat keinen Feed, von dem es erben könnte, es muss also seinen eigenen Autor tragen. Jede Personenkonstruktion braucht außerdem einen <name>: Ein <author> mit nur einer E-Mail-Adresse ist nach Abschnitt 3.2.1 ein Fehler.
Datumsangaben und ids, genau so wie die RFC sie schreibt
Atom-Zeitstempel sind RFC 3339, und Abschnitt 3.3 fügt zwei Anforderungen hinzu. Das Trennzeichen muss ein großes T sein, und ohne numerischen Versatz muss die Zone ein großes Z sein. Damit ist 2026-03-14T09:30:00Z gültig und 2026-03-14t09:30:00z nicht, obwohl jede Datumsbibliothek es parsen würde. Kleinschreibung bekommt eine eigene Meldung. Ein bloßes Datum ist ebenfalls ungültig.
Eine id muss eine absolute IRI sein, sie braucht also ein Schema: https:, tag: und urn: taugen alle, ein relativer Pfad oder eine nackte Zeichenkette nicht. Doppelte ids über Entries hinweg sind ein Fehler, denn Reader entdoppeln anhand der id.
Ids, die sich nur in Groß- und Kleinschreibung, einem abschließenden Schrägstrich oder einem Standardport unterscheiden, erzeugen eine Warnung, denn ids werden Zeichen für Zeichen verglichen: /p/1 und /p/1/ sind zwei Entries. Das tag-Schema, RFC 4151, vermeidet das, und tag:example.com,2026:post-4192 übersteht einen Umzug auf eine neue Domain.
Atom in Code erzeugen und prüfen
Die vier Regeln, die sich zu automatisieren lohnen, sind genau die, die ein Schema nicht ausdrücken kann: die RFC-3339-Schreibweise, die Eindeutigkeit der ids, die Vererbung von author und die Alternative content oder alternate-Link. Jedes Beispiel prüft diese, in der sicheren Parse-Form.
// 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.xmlDas RELAX-NG-Schema in RFC 4287 ist es wert, ausgeführt zu werden, aber kennen Sie seine Grenzen. Es ist informativ und nicht normativ, und eine Grammatik kann keine elementübergreifenden Bedingungen ausdrücken: Vererbung von author, Eindeutigkeit der ids und die summary-Regel müssen alle im Code geprüft werden.
Häufige Fragen
Was ist der Unterschied zwischen RSS und Atom?
Atom ist das jüngere und das strengere Format. Es ist ein IETF-Standard, RFC 4287, veröffentlicht im Dezember 2005; RSS 2.0 wurde 2002 eingefroren und wird als Spezifikationsdokument gepflegt, nicht als Standard.
Die Unterschiede, auf die es ankommt, drehen sich um Mehrdeutigkeit. In RSS kann niemand sagen, ob eine <description> Text oder HTML enthält; in Atom erklärt jede Textkonstruktion ihren Typ. RSS macht die Item-Identität optional und lässt mehrere Datumsschreibweisen zu; Atom verlangt eine dauerhafte id und ein einziges Datumsformat mit Zone.
Warum meldet der Validator, mein Entry habe keinen Autor, obwohl der Feed einen hat?
Sollte er nicht, und wenn doch, steht das author-Element vermutlich nicht dort, wo Sie denken. Die hier umgesetzte Regel ist die Kette aus RFC 4287: der eigene <author> des Entrys, oder einer in seinem <atom:source>, oder einer am <feed>-Element.
Die übliche Ursache der Überraschung ist der Namensraum. Ein <author>, der nicht in http://www.w3.org/2005/Atom liegt, etwa ein dc:creator aus Dublin Core, ist kein Atom-author und erfüllt die Regel nicht. Die andere ist die Platzierung: Ein <author> innerhalb des ersten <entry> deckt nur dieses Entry ab.
Warum ist 2026-03-14 kein gültiges Atom-Datum?
Weil RFC 4287 Abschnitt 3.3 ein vollständiges RFC-3339-Datum mit Uhrzeit verlangt, kein Datum. Atom-Zeitstempel brauchen die Uhrzeit und eine Zone: 2026-03-14T09:30:00Z, oder 2026-03-14T09:30:00+05:30 für einen Versatz.
Zwei weitere Details sind ausdrückliche MUSTs. Das Trennzeichen muss ein großes T sein, und ohne numerischen Versatz muss die Zone ein großes Z sein. Ein kleines t oder z parst in jeder Datumsbibliothek anstandslos und ist trotzdem ungültig – deshalb bekommt es eine eigene Fehlermeldung.
Was sollte ich als id verwenden?
Etwas global Eindeutiges, das Sie nie ändern müssen. Die Spezifikation ist unmissverständlich: Eine id darf sich nicht ändern, wenn das Entry verschoben oder erneut veröffentlicht wird, denn Reader vergleichen ids Zeichen für Zeichen, um zu entscheiden, was neu ist.
Das tag-URI-Schema, RFC 4151, wurde dafür entworfen. tag:example.com,2026:post-4192 ist eindeutig, soll gar nicht auflösen und bindet das Entry nicht daran, wo es gerade liegt. Eine Seiten-URL funktioniert, legt Sie aber fest: Ein zusätzlicher abschließender Schrägstrich oder der Umzug auf https ändert die Identität jedes Entrys, und Abonnenten bekommen das Archiv erneut.
Wird mein Feed irgendwohin geschickt, wenn ich ihn hier validiere?
Nein. Der Parser und jede RFC-4287-Regel sind JavaScript, das in diesem Tab läuft. Es gibt keine Serverkomponente und keine Analytics mit Zugriff auf den Editor. Öffnen Sie das Netzwerkpanel und validieren Sie etwas: Die Seiten-Assets laden einmal, danach nichts mehr.
Die Feeds, die Leute in einen Validator einfügen, sind die, die noch nicht laufen: eine nicht gestartete Seite, ein Feed, dessen Links Zugriffstoken tragen, ein Kundenfeed unter Geheimhaltung. Der W3C-Dienst ist serverseitig, alles dort Geprüfte wird also übertragen. Abrufen geht hier von Ihrem Browser direkt an den von Ihnen genannten Host.
Kann es einen Atom-0.3-Feed prüfen?
Nein, und es sagt das deutlich. Atom 0.3 benutzte den Namensraum http://purl.org/atom/ns#, und ein Feed, der ihn deklariert, wird als Fehler gemeldet, der die Version benennt – samt des 1.0-Namensraums, den Sie stattdessen brauchen.
Atom 0.3 war ein vornormativer Entwurf, den RFC 4287 im Jahr 2005 ersetzte, und dazwischen änderten sich Elementnamen: 0.3 hat <modified> und <issued>, wo 1.0 <updated> und <published> hat, und aus <tagline> wurde <subtitle>. Nur den Namensraum zu ändern erzeugt ein Dokument, das auf neue Weise ungültig ist – die Meldung rät deshalb, auch die Elementnamen durchzusehen.