Warning : The AriaML specification is currently published as an Editor's Draft. This document is part of an ongoing research and development process. The syntax, the architecture of Behavior Sheets, and the lifecycle rules are subject to significant changes. This draft should not be considered stable or ready for production use.

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).

TXT
// --- 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"
});

// --- 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.

TXT
/**
 * 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:

TXT
- 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.