Validateur de flux Atom

La RFC 4287 vérifiée sérieusement, héritage de author compris.

Récupéré directement depuis l’origine. Beaucoup de serveurs le bloquent ; si c’est le cas, collez le contenu.
Entrée
En attenteCollez un document pour le vérifier. La validation se fait à la frappe.

Tout s'exécute dans cet onglet. Rien de ce que vous collez n'est envoyé, journalisé ni transmis où que ce soit. Ouvrez votre panneau réseau et vérifiez.

Collez un flux Atom ci-dessus, ou récupérez-le par URL : il est contrôlé au regard de la RFC 4287. Chaque constat cite la section dont il provient. Les documents d’entrée Atom autonomes, dont la racine est <entry> et non <feed>, sont reconnus et contrôlés selon les règles d’entrée.

Atom est plus strict que RSS, et c’est tout son intérêt : presque tout ce que RSS a laissé à l’interprétation, Atom l’a tranché. Exactement un id, un title et un updated sur le flux et sur chaque entrée. Un seul format de date, sans variante. Chaque construction de texte déclare si elle est du texte, du HTML ou du XHTML.

Une règle est ici implémentée correctement, que la plupart des outils sautent : l’héritage de author. Une entrée n’a pas besoin de son propre auteur si elle possède un <atom:source> qui en porte un, ou si le flux en a un. Ne vérifier que entry/author produit une page d’erreurs fausses sur un flux valide ; ne rien vérifier laisse passer une vraie défaillance.

Pourquoi Atom existe, et en quoi il diffère de RSS

RSS 2.0 a été gelé en 2002, et ses ambiguïtés avec lui. La plus grande était <description> : la spécification n’a jamais dit s’il contenait du texte brut ou du HTML, chaque lecteur devinait donc. L’identité de l’item était facultative. Les dates suivaient la RFC 822, qui autorise les années à deux chiffres et l’absence totale de fuseau.

Atom a été rédigé à l’IETF en réponse et publié comme RFC 4287 en décembre 2005. Le texte porte un type explicite. Chaque flux et chaque entrée ont un id obligatoire et permanent. Les dates sont en RFC 3339 avec fuseau obligatoire. Tout tient dans un seul espace de noms, si bien qu’une extension ne peut pas entrer en collision avec le vocabulaire du cœur.

  • RSS : description peut être du texte ou du HTML, nul ne sait lequel. Atom : le type est déclaré.
  • RSS : guid facultatif et souvent instable. Atom : id obligatoire, IRI absolue, permanent.
  • RSS : dates RFC 822, fuseau facultatif. Atom : RFC 3339, fuseau obligatoire.

Ce que doivent contenir un flux et ses entrées

Exactement un <id>, un <title> et un <updated> sur le flux, et les mêmes trois sur chaque entrée. Pas un ou plusieurs : exactement un, donc un doublon est aussi une erreur. La défaillance la plus courante est un flux avec un titre et des entrées mais sans id.

Une entrée sans <content> doit porter au moins un <link rel="alternate"> : elle doit offrir au lecteur soit la chose elle-même, soit un moyen de l’atteindre. Si <content> a un attribut src, l’élément doit être vide et l’entrée a besoin d’un <summary>, ce qui vaut aussi pour du contenu en base64.

Chaque <link> a besoin d’un href, et rien ne peut porter deux liens rel="alternate" partageant le même type et le même hreflang, car un lecteur ne saurait choisir entre eux. Un rel absent vaut alternate. Un flux devrait aussi porter un <link rel="self">, que la RFC 4287 formule comme un SHOULD : c’est donc un avertissement étiqueté.

La règle d’héritage de author

La RFC 4287 exige qu’un auteur soit résoluble pour chaque entrée, mais pas que l’élément se trouve sur l’entrée. La règle est une chaîne de repli : le <author> propre à l’entrée, ou un <author> à l’intérieur de son <atom:source>, ou un <author> sur le <feed>. Le cas source existe pour les agrégateurs qui republient des entrées collectées ailleurs.

Presque aucun validateur gratuit n’implémente les trois étapes. Ceux qui ne vérifient que entry/author signalent une erreur sur chaque entrée d’un flux de blog mono-auteur parfaitement valide, la forme de flux Atom la plus répandue qui soit. Cet outil parcourt la chaîne et ne signale une erreur que lorsque les trois échouent.

Un document d’entrée Atom autonome n’a pas de flux dont hériter : il doit donc porter son propre auteur. Toute construction de personne a également besoin d’un <name> : un <author> ne contenant qu’une adresse e-mail est une erreur au titre de la section 3.2.1.

Dates et id, exactement comme la RFC les écrit

Les horodatages Atom sont en RFC 3339, et la section 3.3 ajoute deux exigences. Le séparateur doit être un T majuscule, et sans décalage numérique le fuseau doit être un Z majuscule. Donc 2026-03-14T09:30:00Z est valide et 2026-03-14t09:30:00z ne l’est pas, même si toutes les bibliothèques de dates l’analyseraient. Les minuscules ont leur propre message. Une date seule est invalide aussi.

Un id doit être une IRI absolue : il lui faut un schéma. https:, tag: et urn: conviennent tous ; un chemin relatif ou une chaîne nue, non. Des id en double d’une entrée à l’autre sont une erreur, puisque les lecteurs dédupliquent sur l’id.

Des id ne différant que par la casse, une barre oblique finale ou un port par défaut produisent un avertissement, car les id sont comparés caractère par caractère : /p/1 et /p/1/ sont deux entrées. Le schéma tag, RFC 4151, évite cela, et tag:example.com,2026:post-4192 survit à un déménagement de domaine.

Produire et vérifier de l’Atom en code

Les quatre règles qui méritent d’être automatisées sont celles qu’un schéma ne peut pas exprimer : la casse RFC 3339, l’unicité des id, l’héritage de author, et l’alternative content ou lien alternate. Chaque exemple vérifie cela, dans la forme d’analyse sûre.

// 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'))         # ...Z
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.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.xml

Le schéma RELAX NG de la RFC 4287 vaut la peine d’être exécuté, mais connaissez ses limites. Il est informatif et non normatif, et une grammaire ne peut pas exprimer des contraintes qui traversent plusieurs éléments : l’héritage de author, l’unicité des id et la règle du summary doivent tous être vérifiés en code.

Questions fréquentes

Quelle est la différence entre RSS et Atom ?

Atom est le format le plus récent et le plus strict. C’est une norme de l’IETF, la RFC 4287, publiée en décembre 2005 ; RSS 2.0 a été gelé en 2002 et est maintenu comme document de spécification plutôt que comme norme.

Les différences qui comptent portent sur l’ambiguïté. En RSS, nul ne peut dire si une <description> contient du texte ou du HTML ; en Atom, chaque construction de texte déclare son type. RSS rend l’identité de l’item facultative et permet plusieurs écritures de date ; Atom exige un id permanent et un seul format de date avec fuseau.

Pourquoi le validateur dit-il que mon entrée n’a pas d’auteur alors que le flux en a un ?

Il ne devrait pas, et s’il le fait, l’élément author n’est probablement pas là où vous croyez. La règle implémentée ici est la chaîne de la RFC 4287 : le <author> propre à l’entrée, ou un <author> dans son <atom:source>, ou un <author> sur l’élément <feed>.

La cause habituelle de la surprise est l’espace de noms. Un <author> qui n’est pas dans http://www.w3.org/2005/Atom, un dc:creator de Dublin Core par exemple, n’est pas un author Atom et ne satisfait pas la règle. L’autre est le placement : un <author> à l’intérieur de la première <entry> ne couvre que cette entrée.

Pourquoi 2026-03-14 n’est-il pas une date Atom valide ?

Parce que la section 3.3 de la RFC 4287 exige une date-heure RFC 3339 complète, pas une date. Les horodatages Atom réclament l’heure et un fuseau : 2026-03-14T09:30:00Z, ou 2026-03-14T09:30:00+05:30 pour un décalage.

Deux détails supplémentaires sont des MUST explicites. Le séparateur doit être un T majuscule, et sans décalage numérique le fuseau doit être un Z majuscule. Un t ou un z minuscule s’analyse parfaitement dans toutes les bibliothèques de dates et reste invalide, d’où son message d’erreur dédié.

Que faut-il utiliser comme id ?

Quelque chose de globalement unique que vous n’aurez jamais besoin de changer. La spécification est catégorique : un id ne doit pas changer quand l’entrée est déplacée ou republiée, car les lecteurs comparent les id caractère par caractère pour décider de ce qui est nouveau.

Le schéma de tag URI, RFC 4151, a été conçu pour cela. tag:example.com,2026:post-4192 est unique, n’est pas censé se résoudre, et n’attache pas l’entrée à l’endroit où elle vit aujourd’hui. Une URL de page fonctionne mais vous engage : ajouter une barre oblique finale ou passer à https change l’identité de chaque entrée, et les abonnés reçoivent de nouveau les archives.

Mon flux est-il envoyé quelque part quand je le valide ici ?

Non. L’analyseur et chaque règle de la RFC 4287 sont du JavaScript exécuté dans cet onglet. Il n’y a pas de composant serveur ni d’analytics ayant accès à l’éditeur. Ouvrez le panneau réseau et validez quelque chose : les ressources de la page se chargent une fois, puis plus rien.

Les flux que les gens collent dans un validateur sont ceux qui ne marchent pas encore : un site non lancé, un flux dont les liens portent des jetons d’accès, un flux client sous accord de confidentialité. Le service du W3C est côté serveur, donc tout ce qu’on y vérifie est transmis. Ici, Récupérer va de votre navigateur directement à l’hôte que vous avez nommé.

Peut-il vérifier un flux Atom 0.3 ?

Non, et il le dit clairement. Atom 0.3 utilisait l’espace de noms http://purl.org/atom/ns#, et un flux qui le déclare est signalé par une erreur nommant la version, accompagnée de l’espace de noms 1.0 qu’il vous faut à la place.

Atom 0.3 était un brouillon pré-normatif que la RFC 4287 a remplacé en 2005, et des noms d’éléments ont changé entre-temps : 0.3 a <modified> et <issued> là où 1.0 a <updated> et <published>, et <tagline> est devenu <subtitle>. Ne changer que l’espace de noms produit un document invalide d’une nouvelle manière : le message vous invite donc à revoir aussi les noms d’éléments.

Outils associés

Pour aller plus loin