How to implement AriaML on the server side?

Allow a server to generate a stream compatible with AriaML fluid navigation while remaining backward compatible with standard HTML clients.

This documentation provides a standard implementation strategy for an AriaML SSR Polyfill. The goal is to allow a server to generate a stream compatible with AriaML fluid navigation while remaining backward compatible with standard HTML clients.

IMPORTANT: A PHP implementation already exists.


1. Conceptual Architecture

It is recommended to opt for a decoupled strategy using three objects:

  1. Request Factory: Analyzes client intent (Headers).
  2. AriaML Document: Structures semantic data (JSON-LD) and manages the transport markup.
  3. Response Factory: Synchronizes document state and output headers.

2. Communication Protocol (Headers)

The server must specifically react to the following headers sent by the client:

Header Value / Type Role
Accept text/aria-ml-fragment Highest priority: requests a native rendering (without <html>).
X-AriaML-Fragment flag Optional fallback to force fragment mode (in case of Accept header corruption).
nav-cache JSON Array List of DOM element keys already present in the client cache.
ariaml-force-html boolean If true, the server must respond with text/html (Web Extension).

3. Class Specifications

A. AriaMLRequestFactory

This class acts as a context analyzer.

  • isFragment(): Returns true if Accept starts with text/aria-ml-fragment or if X-AriaML-Fragment is present.
  • expectsHtmlWrapper(): Returns false if a fragment is requested OR if the client natively accepts text/aria-ml. Returns true for a standard browser (SEO).
  • expectedStatus(): Returns 206 Partial Content if isFragment() is true, otherwise 200 OK.
  • expectedContentType():
  • If ariaml-force-html == true: always text/html.
  • Otherwise: text/aria-ml-fragment (for fragments) or text/aria-ml (for full documents).

B. AriaMLDocument

The structure generator.

  • Attributes: isFragment (bool), expectedHtml (bool).
  • startTag():
    • If expectedHtml, generates <!DOCTYPE html><html...><head>...[SSR Head]...</head><body>.
    • Opens <aria-ml-fragment> if isFragment, otherwise <aria-ml>.
  • endTag():
    • Closes </aria-ml-fragment> or </aria-ml>.
    • If expectedHtml, injects <script src="standalone.js"></script></body></html>.
  • consumeDefinition(keys): Returns a portion of the JSON-LD while avoiding returning keys already marked as "consumed." It is often beneficial to split the JSON-LD into two blocks. One is dynamic and updated with every context change; the other is static and intended for search engine crawlers. (See below).

C. AriaMLResponseFactory

The synchronizer (Middleware).

  • applyTo(request, document):
  1. Injects the request state into the document (isFragment, expectedHtml).
  2. Sets the HTTP code (206 or 200).
  3. Applies the Vary: Accept, X-AriaML-Fragment, nav-cache header (crucial for browser caching).
  4. Sets the Content-Type.

4. "Deep Restoration" Algorithm (SSR side)

When generating HTML inside slots, the server must check the client cache to save bandwidth.

Component Rendering Logic:

  1. Retrieve the cacheKey of the component.
  2. Check if cacheKey exists in the nav-cache array of the request.
  3. If present: Send only <div nav-cache="my-key"></div> (empty).
  4. If absent: Send the full content <div nav-cache="my-key">...content...</div>.

AriaML will detect the empty element with the nav-cache attribute and automatically re-inject the live DOM stored locally.


5. Implementation Example (Pseudo-Code / Node.js)

1. Process-Based Approach (Sequential)

This approach details how AriaML objects interact with the native req and res objects of a server (Express/Node.js style).

// --- Step 1: Analyze client intent ---
const reqFactory = new AriaMLRequestFactory(req.headers);

// --- Step 2: Prepare the Document ---
const doc = new AriaMLDocument({
    "name": "Product Page",
    "inLanguage": "en-US",
    "direction": "ltr",
    "url": "[https://mysite.com/shoes](https://mysite.com/shoes)"
});

// --- Step 3: Synchronization (ResponseFactory configures everything) ---
const respFactory = new AriaMLResponseFactory();

// This method configures res.status, res.setHeader
// and sets doc.isFragment / doc.expectedHtml
respFactory.applyTo(reqFactory, doc, res);

// --- Step 4: Stream Rendering ---
res.write(doc.startTag());

// Injecting consumable definitions
res.write(`
    <script type="application/ld+json" nav-slot="dynamic-definition">
        ${doc.consumeDefinition(['name', 'inLanguage', 'direction'])}   </script>
`);

if(!reqFactory.isFragment()) {
    res.write(`
        <script type="application/ld+json">
            ${doc.consumeDefinition()} </script>
    `);
}

// Slot Logic with 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>Full Content</h1></div>');
}
res.write('</main>');

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

2. Generic Approach (Handler / Middleware)

Once the process is understood, it can be isolated so that controllers only manage business data.

/**
 * Generic AriaML Handler
 * This function encapsulates the AriaML plumbing.
 */
async function ariaMLHandler(req, res, data, renderContent) {
    const reqFactory = new AriaMLRequestFactory(req.headers);
    const doc = new AriaMLDocument(data);
    const respFactory = new AriaMLResponseFactory();

    // Automatic synchronization
    respFactory.applyTo(reqFactory, doc, res);

    // Start stream
    res.write(doc.startTag());

    // System metadata rendering
    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>
    `);

    // Delegate content to controller
    // The controller receives doc and reqFactory to manage its own slots/cache
    await renderContent(doc, reqFactory);

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

6. Implementation Points of Interest

  • Immutability: If using immutable response objects (like PHP PSR-7 or certain Node frameworks), ensure applyTo returns a new response instance.
  • Encoding: JSON-LD injected via consumeDefinition must be escaped to prevent XSS while remaining readable by JSON.parse on the client side.
  • Header Order: The Vary header allow to prevent CDNs or browser caches from serving a fragment instead of a full page.

To port this engine to a new language, keep these rules in mind:

A. Negotiation (ResponseFactory)

The server must produce these headers according to the AriaMLRequestFactory state:

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

B. Consumption (AriaMLDocument)

The consumeDefinition(keys) method must be stateful:

  • It stores an internal list of keys already sent.
  • If keys is empty, it iterates through the original JSON-LD and only returns what is not in the consumed keys list.
  • It must produce valid JSON intended to be wrapped in a <script type="application/ld+json"> block.

C. Conditional Rendering (Deep Restoration)

To enable AriaML bandwidth saving:

  • ALWAYS check reqFactory.clientHasCache(key) before generating the HTML of a block possessing a nav-cache attribute.
  • If true, the server should only return the empty "shell": <tag nav-cache="key"></tag>.
  • Note that an element can have both the nav-cache attribute and the nav-slot attribute.