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:
- Request Factory: Analyzes client intent (Headers).
- AriaML Document: Structures semantic data (JSON-LD) and manages the transport markup.
- 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(): ReturnstrueifAcceptstarts withtext/aria-ml-fragmentor ifX-AriaML-Fragmentis present.expectsHtmlWrapper(): Returnsfalseif a fragment is requested OR if the client natively acceptstext/aria-ml. Returnstruefor a standard browser (SEO).expectedStatus(): Returns206 Partial ContentifisFragment()is true, otherwise200 OK.expectedContentType():- If
ariaml-force-html == true: alwaystext/html. - Otherwise:
text/aria-ml-fragment(for fragments) ortext/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>ifisFragment, otherwise<aria-ml>.
- If
endTag():- Closes
</aria-ml-fragment>or</aria-ml>. - If
expectedHtml, injects<script src="standalone.js"></script></body></html>.
- Closes
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):
- Injects the request state into the document (
isFragment,expectedHtml). - Sets the HTTP code (
206or200). - Applies the
Vary: Accept, X-AriaML-Fragment, nav-cacheheader (crucial for browser caching). - 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:
- Retrieve the
cacheKeyof the component. - Check if
cacheKeyexists in thenav-cachearray of the request. - If present: Send only
<div nav-cache="my-key"></div>(empty). - 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
applyToreturns a new response instance. - Encoding: JSON-LD injected via
consumeDefinitionmust be escaped to prevent XSS while remaining readable byJSON.parseon the client side. - Header Order: The
Varyheader 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
keysis 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 anav-cacheattribute. - If
true, the server should only return the empty "shell":<tag nav-cache="key"></tag>. - Note that an element can have both the
nav-cacheattribute and thenav-slotattribute.