Comment implémenter AriaML côté serveur?

Permettre à un serveur de générer un flux compatible avec le la navigation fluide AriaML, tout en restant rétrocompatible avec les clients Html.

Cette documentation fournit une stratégie standard d'implémentation d'un Polyfill SSR AriaML. L'objectif est de permettre à un serveur de générer un flux compatible avec la navigation fluide AriaML, tout en restant rétrocompatible avec les clients Html.

IMPORTANT Une implémentation PHP existe déjà.


1. Architecture Conceptuelle

Il est recommandée d'opter pour une stratégie découplée en trois objets :

  1. Request Factory : Analyse l'intention du client (Headers).
  2. AriaML Document : Structure les données sémantiques (JSON-LD) et gère le balisage de transport.
  3. Response Factory : Synchronise l'état du document et les headers de sortie.

2. Le Protocole de Communication (Headers)

Le serveur doit réagir spécifiquement aux headers suivants envoyés par le client :

Header Valeur / Type Rôle
Accept text/aria-ml-fragment Priorité maximale : demande un rendu natif (sans <html>).
X-AriaML-Fragment flag Fallback facultatif pour forcer le mode fragment (en cas de corruption du header accept).
nav-cache JSON Array Liste des clés d'éléments DOM déjà présents dans le cache client.
ariaml-force-html boolean Si true, le serveur doit répondre en text/html (Web Extension).

3. Spécifications des Classes

A. AriaMLRequestFactory

Cette classe est un analyseur de contexte.

  • isFragment() : Retourne true si Accept commence par text/aria-ml-fragment ou si X-AriaML-Fragment est présent.
  • expectsHtmlWrapper() : Retourne false si un fragment est demandé OU si le client accepte nativement text/aria-ml. Retourne true pour un navigateur standard (SEO).
  • expectedStatus() : Retourne 206 Partial Content si isFragment() est vrai, sinon 200 OK.
  • expectedContentType() :
    • Si ariaml-force-html == true : toujours text/html.
    • Sinon : text/aria-ml-fragment (pour fragment) ou text/aria-ml (pour document complet).

B. AriaMLDocument

Le générateur de structure.

  • Attributs : isFragment (bool), expectedHtml (bool).
  • startTag() :
    • Si expectedHtml, génère <!DOCTYPE html><html...><head>...[SSR Head]...</head><body>.
    • Ouvre <aria-ml-fragment> si isFragment, sinon <aria-ml>.
  • endTag() :
    • Ferme </aria-ml-fragment> ou </aria-ml>.
    • Si expectedHtml, injecte <script src="standalone.js"></script></body></html>.
  • consumeDefinition(keys) : Retourne une portion du JSON-LD en évitant de renvoyer les clés déjà marquées comme "consommées". Il peut être interessant de découper le JSON-LD, généralement en deux blocs. L'un d'eux est dynamique et actualisé à chaque changement de contexte, l'autre est statique et s'adresse aux robots d'indexation des moteurs de recherches. (voire plus bas)

C. AriaMLResponseFactory

Le synchroniseur (Middleware).

  • applyTo(request, document) :
  1. Injecte l'état de la requête dans le document (isFragment, expectedHtml).
  2. Définit le code HTTP (206 ou 200).
  3. Applique le header Vary: Accept, X-AriaML-Fragment, nav-cache (crucial pour le cache navigateur).
  4. Définit le Content-Type.

4. Algorithme de "Deep Restoration" (SSR side)

Lors de la génération du HTML à l'intérieur des slots, le serveur doit vérifier le cache client pour économiser la bande passante.

Logique de rendu d'un composant :

  1. Récupérer la cacheKey du composant.
  2. Vérifier si cacheKey existe dans l'array nav-cache de la requête.
  3. Si présente : Envoyer uniquement <div nav-cache="ma-clef"></div> (vide).
  4. Si absente : Envoyer le contenu complet <div nav-cache="ma-clef">...contenu...</div>.

AriaML détectera l'élément vide avec l'attribut nav-cache et réinjectera automatiquement le DOM vivant stocké en local.


5. Exemple d'implémentation (Pseudo-Code / Node.js)

1. Approche par Processus (Séquentielle)

Cette approche détaille comment les objets AriaML interagissent avec les objets natifs req et res d'un serveur (ici type Express/Node.js).

// --- Étape 1 : Analyse de l'intention client ---
const reqFactory = new AriaMLRequestFactory(req.headers);

// --- Étape 2 : Préparation du Document ---
const doc = new AriaMLDocument({
    "name": "Page Produit",
    "inLanguage": "fr-FR",
    "direction": "ltr",
    "url": "[https://monsite.com/chaussures](https://monsite.com/chaussures)"
});

// --- Étape 3 : Synchronisation (La ResponseFactory configure le tout) ---
const respFactory = new AriaMLResponseFactory();

// Cette méthode configure res.status, res.setHeader
// et définit doc.isFragment / doc.expectedHtml
respFactory.applyTo(reqFactory, doc, res);

// --- Étape 4 : Rendu du flux ---
res.write(doc.startTag());

// Injection des définitions consommables
res.write(`
    <script type="application/ld+json" nav-slot="dynamic-definition">
        ${doc.consumeDefinition(['name', 'inLanguage', 'direction'])}   <!-- dynamique, actualisé à chaque changement de contexte -->
    </script>
`);
if(!reqFactory.isFragment())
    res.write(`
        <script type="application/ld+json">
            ${doc.consumeDefinition()} <!-- tout le reste : s'adresse aux robots d'indexation des moteurs de recherches -->
        </script>
    `);

// Logique de Slot avec Deep Restoration
res.write('<main nav-slot="content">');
if (reqFactory.clientHasCache('main-view')) {
    res.write('<div nav-cache="main-view"></div>');
} else {
    res.write('<div nav-cache="main-view"><h1>Contenu complet</h1></div>');
}
res.write('</main>');

res.write(doc.endTag());
res.end();

2. Approche Générique (Handler / Middleware)

Une fois le process compris, on l'isole pour permettre aux contrôleurs de ne gérer que la donnée métier.

/**
 * AriaML Handler générique
 * Cette fonction encapsule la plomberie AriaML.
 */
async function ariaMLHandler(req, res, data, renderContent) {
    const reqFactory = new AriaMLRequestFactory(req.headers);
    const doc = new AriaMLDocument(data);
    const respFactory = new AriaMLResponseFactory();

    // Synchronisation automatique
    respFactory.applyTo(reqFactory, doc, res);

    // Début du stream
    res.write(doc.startTag());

    // Rendu des métadonnées système
    res.write(`
        <script type="application/ld+json" nav-slot="dynamic-definition">
            ${doc.consumeDefinition(['name', 'inLanguage', 'direction'])}
        </script>
        <script type="application/ld+json">
            ${doc.consumeDefinition()}
        </script>
    `);

    // Délégation du contenu au contrôleur
    // Le contrôleur reçoit doc et reqFactory pour gérer ses propres slots/cache
    await renderContent(doc, reqFactory);

    res.write(doc.endTag());
    res.end();
}

6. Points de vigilance pour l'implémentation

  • Immuabilité : Si vous utilisez des objets de réponse immuables (comme en PHP PSR-7 ou avec certains frameworks Node), assurez-vous que applyTo retourne une nouvelle instance de la réponse.
  • Encodage : Le JSON-LD injecté via consumeDefinition doit être échappé pour éviter les injections XSS, tout en restant lisible par JSON.parse côté client.
  • Ordre des Headers : Le header Vary permet d'éviter que les CDN ou les caches navigateurs ne servent un fragment à la place d'une page complète.

Pour porter ce moteur dans un nouveau langage, gardez ces règles en tête :

A. La Négociation (ResponseFactory)

Le serveur doit produire les headers suivant selon l'état de AriaMLRequestFactory :

- Vary: Accept, X-AriaML-Fragment, nav-cache
- Content-Type: [text/aria-ml | text/aria-ml-fragment | text/html]
- Status: [200 | 206]

B. La Consommation (AriaMLDocument)

La méthode consumeDefinition(keys) doit être à état (stateful) :

  • Elle stocke une liste interne des clés déjà envoyées.
  • Si keys est vide, elle itère sur le JSON-LD original et ne renvoie que ce qui n'est pas dans la liste des clés consommées.
  • Elle doit produire un JSON valide, déstiné à être wrappé dans un bloc <script type="application/ld+json">.

C. Le Rendu Conditionnel (Deep Restoration)

Afin de permettre l'économie de bande passante AriaML :

  • TOUJOURS vérifier reqFactory.clientHasCache(key) avant de générer le HTML d'un bloc possédant un attribut nav-cache.
  • Si true, le serveur ne devrait renvoyer que la "coquille" vide : <tag nav-cache="key"></tag>.
  • Notons qu'un élémént peut avoir à la fois l'attribut nav-cache et l'attribut nav-slot.