Documentation · OF1 Product
The page your visitor sees isn't the page you published. OF1 Product turns a static site into a surface that adapts per visitor. This page explains how a page is generated, what signals drive it, what data is (and isn't) stored, and how to integrate the service into your own site. It describes the of1-gen-web-service — the runtime that produces personalized pages.
What of1 is
of1 is an AI service that assembles a web page in real time for the individual visitor in front of it. Instead of serving one page authored once by hand, it reads the visitor's context, works out what they're trying to do, retrieves the most relevant content from your own corpus, and composes a page that fits that intent — every time, with no rules to write and no segments to maintain.
The critical design decision is that the LLM never writes HTML. It decides what to show and in what order; your design system decides how it renders. That keeps output on-brand, accessible, and impossible to break — while still being fully personalized.
of1 runs on Adobe Edge Delivery Services — Adobe's edge-native web platform. Edge Delivery Services is a hard requirement: of1 assembles pages from the block-based content model that EDS provides and delivers them at the edge.
Adapts
Per visitor, per intent
Every element is reconsidered for the person viewing it — not a segment they were bucketed into.
On-brand
Blocks, not markup
The model selects from 30+ pre-built, CSS-styled blocks. It never invents new markup.
Grounded
Your content, via RAG
Copy is filled from your products, features and FAQs — retrieved by vector search, not hallucinated.
Private
Signals stay client-side
Behavioural context is gathered in the browser and sent per request. of1 stores no visitor profile.
The core idea
For decades, marketing meant shouting at categories and cohorts. Your visitors now spend all day with tools that respond to their intent — then land on your one static page.
of1 removes the segment. There are no rules to write, no content matrix to maintain, no A/B variants to fork. The page is generated for the one person viewing it — and it should feel authored, not targeted. As the deck puts it: most AI wants you to know it's there; ours wants you to forget.
Request lifecycle
A single call to POST /api/generate runs an ordered pipeline at the edge. Results stream back as newline-delimited JSON (NDJSON) so the page renders block-by-block as it's produced.
- Capture context — at the edge, from the browser. The request carries the visitor's
query, plus optional signals:browsing[](URL, dwell time, scroll depth),interests[], a pre-classifiedintent,segment(device, region) andacquisition(UTM, referrer). A tenant id identifies the brand. - Analyze behaviour — classifies the traffic source (shopping / social / search / email from the referrer) and derives a journey stage — awareness → consideration → decision — from session depth and dwell time. This frames the tone and depth of the page.
- Classify intent — the query is sorted into one of
comparison,recommendation,deep-dive,budgetordiscovery, by LLM (default) or keyword fallback. Personas and use-cases configured for the tenant are matched in parallel. - Retrieve content (RAG) — the query is embedded and used to search a per-tenant vector index over your products, features and FAQs. Up to ten ranked, relevance-scored results are returned — this is the grounded content the page is built from.
- Build the prompt — brand voice, optional brand-governance rules, the block/template guide, the retrieved content, and the analyzed intent are compiled into a system + user prompt for the model.
- Generate & stream — the model emits structured JSON describing the page. The worker streams it as NDJSON events —
section(one per block),suggestions(follow-up prompts),debug(timings, model, token usage) anddone— so the client renders progressively.
The two-phase LLM
When template routing is enabled for a tenant, the model runs twice: first it picks a layout that fits the intent, then it fills that layout with retrieved, on-brand content.
- Visitor context — Collected at the edge: referrer & query, device & time of day, scroll & click history, in-session intent. Never leaves the browser.
- LLM reasoning engine — Phase 1 · Select a template — A template is a block order that fits the visitor's intent, e.g.
first-full-recommendation→hero,product,compare,verdict. - LLM reasoning engine — Phase 2 · Fill the blocks — Inputs: RAG retrieval (vector search over pages, images, docs) and the brand layer (voice, compliance, design system). Output: every block filled, on-brand.
- Personalized page — Rendered from blocks — feels authored, not generated.
[diagram placeholder — recreate as a visual asset: "From visitor to page · one request, two phases" — visitor context → LLM reasoning engine (two phases) → personalized page. No HTML invented, no CSS touched — the LLM only decides which blocks, in what order, and what fills them.]
In Phase 1 the model classifies intent and picks one template from the tenant library (e.g. first-full-recommendation) — a template is just a fixed order of blocks, and the model never reorders it. In Phase 2 it fills each block's slots from RAG-retrieved content, shaped by the brand layer (voice, compliance, design tokens); image URLs are validated against an allow-list before render.
The output is a complete page — assembled from pre-styled blocks — that feels authored rather than generated. The default flow works the same way without a template step: the model composes hero / columns / cards sections directly.
Why two phases. Separating structure (which blocks, in which order) from content (what goes in them) keeps each model call small and verifiable, and means the layout is always one of a known, tested set.
Blocks & templates
Why the LLM picks blocks instead of writing HTML — and how templates map intent to a layout.
A typical web page is 10,000–50,000 tokens of HTML. Asking an LLM to emit that raw is slow, brittle, and unsafe: it hallucinates class names, breaks CSS and drops accessibility attributes, and every page would need validating. of1 sidesteps this by reusing the separation of concerns every CMS already has:
- Styling — never touched by the LLM. Design tokens + CSS, owned by developers.
- Structure — the LLM chooses. Blocks: which ones, and in what order.
- Content — the LLM fills. Copy, images and data, from your corpus.
[diagram placeholder — recreate as a visual asset: "Separation of concerns · the LLM decides what, not how" — a 3-layer grid: styling / structure / content]
In AEM Edge Delivery Services a block is simply a named table in a document — authors write them in Google Docs or Word, developers style them with CSS. of1 selects from 30+ of these pre-built blocks; it never invents markup. A template is a block order that fits a visitor's intent, grouped into families:
- First recs — Visitor lands cold — surface a confident top pick. Examples:
first-full-recommendation,first-discovery-broad,first-budget-focused - Follow-up — They're comparing — narrow the field, go head-to-head. Examples:
followup-head-to-head,followup-narrow-field,followup-deep-compare - Quick answers — One specific question — answer it, nudge a next step. Examples:
quick-yes-no,quick-with-recommendation,quick-feature-explain
Inside one template — first-full-recommendation — is a fixed block order:
heroproduct-recommendationcomparison-tableverdict-cardrecipe-cards
[diagram placeholder — recreate as a visual asset: "Inside one template · a fixed block order" — the LLM fills each block, it never reorders them.]
Levels of generation
of1 generates at five levels, from picking one approved image to writing a whole page. The levels are independent: a page can use one, several or none, and every level falls back to the page as published. A team that isn't ready to let a model write copy can start at levels 1 and 2, where nothing is written at all.
- Level 1 · Image selection.
{{generative-image: collection}}in an image's alt text. A cosine match against images you've approved; no LLM call. See Generative images and fragments. - Level 2 · Fragment selection.
{{generative-fragment: type}}as a Fragment block's link text. The same match, over your fragments; no LLM call. - Level 3 · Generative components. A block variant, an inline marker or a section scope. The model rewrites only the text you mark. See Generative components.
- Level 4 · Automatic personalization. No markup.
of1-edge-proxyreads the visitor and rewrites a few key elements, such as the headline, subhead and call to action, throughPOST /api/personalize. - Level 5 · Full-page generation.
POST /api/generateassembles a page from your block library and templates, filled from your tenant content. See Request lifecycle.
Levels 1 to 3 share one path through the proxy, and a page that carries any of their markup skips level 4: an author's explicit intent outranks the proxy's guess at which lines matter. Up to 20 marked slots are filled per page, in DOM order.
Generative components
Everything above describes of1-gen-web-service assembling a page from nothing. of1-edge-proxy does the opposite: it fronts a page you've already published and rewrites only the pieces an author marked, leaving structure and CSS exactly as authored.
There's no code to write. An author marks intent using ordinary DA conventions — a block variant, a Section Metadata key, or a directive typed straight into a sentence — and the proxy resolves it into an LLM instruction at request time, per visitor. Three mechanisms cover every shape of authored content: a whole block, one sentence, or plain body text with no block at all.
A · Block variant — whole block, every text cell
Append generative to any block's name and every text cell inside it becomes a slot. Cells that carry only an image are left alone. A Section Metadata generative-instruction key next to it sets one instruction for every cell in the block — otherwise the proxy defaults to "adapt this to the visitor."
B · Inline marker — one sentence, anywhere
{{generate: instruction}} typed directly into a paragraph. Pair it with {{default: fallback}} to control exactly what a reader sees when generation is skipped or fails — a reader must never see raw markup.
S · Section scope — plain body text, no block
For ordinary headings and paragraphs with no block at all, a Section Metadata generative-scope key lists which tags are eligible (h1–h6, p). Fail-closed by design: a section with no scope key generates nothing, however it's styled.
What each mechanism looks like authored in DA:
Mechanism A — a section authored as a Cards (generative) block, with a Section Metadata generative-instruction set alongside it. Every text cell becomes a slot; image cells carry no text and must survive untouched.
Demo A — generative block variant (cards, Spanish)
Authored as Cards (generative), with a section-wide instruction in this section's metadata. Every text cell becomes a slot; the image cells carry no text and must survive untouched.
| cards (generative) | |
|
|
Precision Brewing Every shot is measured to the gram, so the result never depends on guesswork. |
|
|
Built to Last Stainless steel and brass age with grace, not with a replacement cycle. |
| section-metadata | |
| generative-instruction | Translate the text to Spanish. |
Mechanism B — an inline marker typed straight into a paragraph: {{generate: Reframe this for a home barista, two sentences}}{{default: Consistent tamp pressure is the difference between a good shot and a great one. Ten thousand reps later, your wrist remembers what your eyes can't measure.}}
Demo B — inline marker (one sentence, anywhere)
No block, no Section Metadata — authored as an ordinary sentence, with the instruction and its fallback typed straight into the paragraph:
{{generate: Reframe this for a home barista, two sentences}}{{default: Consistent tamp pressure is the difference between a good shot and a great one. Ten thousand reps later, your wrist remembers what your eyes can't measure.}}
Renders as — the fallback shown to this reader: "Consistent tamp pressure is the difference between a good shot and a great one. Ten thousand reps later, your wrist remembers what your eyes can't measure."
Mechanism S — plain body text, opted in by Section Metadata alone: a heading "Water quality" and a paragraph on filtering, with Section Metadata generative-scope set to h3, p and generative-instruction set to "Translate the text to Japanese."
Demo S — section scope, no block (heading + paragraph, Japanese)
Plain body text, opted in by Section Metadata alone — generative-scope lists which tags are eligible. No block, no Section Metadata key, no generation.
Water quality
Filtered to under 50ppm hardness, so the coffee's own flavor leads — not the minerals in your tap.
| section-metadata | |
| generative-scope | h3, p |
| generative-instruction | Translate the text to Japanese. |
Fails open, always. Every mechanism serves the authored fallback — the visible text, or an explicit {{default:}} — on a slow endpoint, a rejected slot id, or missing visitor consent. A reader can never end up with broken markup or a blank block; the worst case is simply the page as published.
Generative images and fragments
Levels 1 and 2 run through the same proxy path as generative components, but they make no LLM call. The proxy embeds the visitor's context once, then picks the closest item by cosine similarity from a set you approved ahead of time. Nothing is written. The only thing that changes is which image or fragment the visitor gets.
Generative images
Type {{generative-image: collection}} into an image's alt text. The collection names a set in your generative-images sheet. The authored image stays in place as the fallback, and the marker is stripped before the page is served, so a reader never sees it.
| hero | |
|
|
Coffee, chosen for youImage alt text: {{generative-image: hero}} |
A visitor arriving from a Christmas campaign gets the gift-guide photo; one who has been reading about cycling gets the cycling photo. With no match, the authored photo stays.
The sheet is a multi-sheet DA document with two tabs: collections (id, ratio, description) and images (id, collection, name, description, tags, alt, url). Each image's name, description and tags are embedded at sync time. A row with no url is left out until the image exists.
Generative fragments
Use {{generative-fragment: type}} as the link text of a Fragment block, and point the link at a real fragment. That link is the fallback: of1 only swaps the href when it finds a better match, and it strips the marker either way.
| fragment |
|
{{generative-fragment: promo-banner}} links to /fragments/promos/spring-sale |
A winter-campaign visitor gets the autumn-ritual promo, a Christmas visitor gets the gift promo, and everyone else keeps the spring sale.
Candidates live in a generative-fragments sheet with a types tab (id, description) and a fragments tab (id, type, url, name, description, blocks, tags). The block list is embedded along with the prose, so a fragment built around a testimonial and one built around a pricing table stay apart even when their descriptions sound alike.
Publish both sheets with the rest of your tenant files (see Tenant configuration) and run a tenant sync.
Picked per visitor, not per segment. Text slots are cached per audience segment for five minutes. Image and fragment picks skip that cache, because they depend on the visitor's own signals, which the segment key leaves out on purpose.
Generative templates (DA blocks)
A generative component rewrites one element on a page that already exists. A template is the other end of the spectrum: the full block order of1-gen-web-service assembles from nothing (see Blocks & templates above). Authoring a template means building the block library it draws from — not the template itself.
Every block name a template references — hero, product-recommendation, comparison-table, verdict-card — is a real DA block: built and CSS-styled once by your developers, exactly like any other EDS block. The LLM only ever picks names out of that library and fills their slots with RAG-retrieved content; it never invents a block and never touches the CSS. Once a block exists in the library, it's reusable across every template that names it.
One block from the library, authored exactly like any other DA block — no generative markup needed here: a Verdict Card block with an image cell and a text cell ("Verdict" / "Our pick for you, in one line — filled at generation time, from your own product data"). Authored once, styled once, then reused as the target shape for every generated page that includes a verdict-card.
Template pages are authored, previewed, and published exactly the same way as any other DA page — hero, comparison-table, spec-badges and product-card below are just page content, nothing generative-specific about the mechanics. Here's a full template page as authored in DA, four sections in one document: Compare Arco Machines.
Compare Arco Machines
Side-by-side comparison to help you choose between two or more Arco espresso machines based on price, boiler type, and use case.
| hero | |
|
|
Find the Right Arco for YouCompare boiler type, heat-up time, and pressure control to see which machine fits your routine. |
| section-metadata | |
| Template Intent | comparison |
| Template Description | Introduces a head-to-head comparison between two to four Arco machines or grinders |
Feature Comparison
| comparison-table | ||
| Feature | Arco Primo | Arco Doppio |
| Price | €299 | €549 |
| Boiler Type | Thermoblock | Dual boiler (brass brew, stainless steam) |
| Heat-Up Time | 90 seconds | 3 minutes |
| Best For | Quick, reliable morning espresso | Milk drinks and precision extraction |
| section-metadata | |
| Template Min Items | 2 |
| Template Max Items | 4 |
Quick Facts
| spec-badges | ||
Quick signals before the full comparisonUse these to narrow down before reading the detailed breakdown above. |
||
| Boiler | Dual boiler | Best when milk drinks are frequent |
| Warm-up | 3 minutes | Fast enough without the compromises of a thermoblock |
| section-metadata | |
| Template Min Items | 3 |
| Template Max Items | 4 |
Shop the Machines
| product-card | |
|
|
Primo€299 Our signature single-boiler machine for the home barista. morning-minimalist, upgrader, non-barista |
|
|
Doppio€549 Dual-boiler performance. Steam and brew simultaneously without compromise. upgrader, morning-minimalist, craft-barista |
| section-metadata | |
| Template Min Items | 2 |
| Template Max Items | 4 |
The template itself isn't a DA page — it's a name and an ordered list of block names, declared in templates.json:
{
··"first-full-recommendation": {
····"blocks": ["hero", "product-recommendation", "comparison-table", "verdict-card", "recipe-cards"]
··}
}
Phase 1 of generation (see The two-phase LLM) picks one template name that fits the visitor's intent; Phase 2 fills each named block in that fixed order. A tenant that skips template routing can instead ship a block-guide.json — a looser set of rules the model composes from directly, without naming a fixed sequence up front.
Two authoring surfaces, one library. Generative components and generative templates draw from the same idea — the LLM chooses what and in what order, never how — but they're separate authoring surfaces. Components mark up a page that already exists, one element at a time. Templates and their blocks are built once, ahead of time, as a reusable library of1-gen-web-service assembles from on every request.
Try it on a page you've already published. Nothing above needs a code change or an SDK embed on an existing EDS site. of1-edge-proxy fronts your site the same way aem.page / aem.live already does — swap the host and both generative components and the /of1 template route work immediately: main--<repo>--<org>.of1.live instead of main--<repo>--<org>.aem.live (or the prev-- prefix for a preview host). For example, https://main--arco2--froesef--dev.of1.live/ serves the same content as the site's aem.live host, but already generative-component- and template-aware.
Signals captured
Context is gathered client-side and passed on the request. It describes the current session — not a stored identity.
- Referrer & query (
acquisition,query) — Where the visitor came from and what they asked - Device & region (
segment) — Form factor and locale for framing - Scroll & click history (
browsing[]) — URL,dwellTimeMs,scrollDepthper page — journey depth - Interests (
interests[]) — Pre-computed topic scores that bias retrieval - In-session intent (
intent) — A pre-classified goal, if the client has one - Prior queries (
conversationHistory[]) — Earlier turns, for de-duplication
Privacy — never leaves the browser. These signals are assembled client-side and sent only as part of the generation request. of1 does not set tracking cookies or build a cross-session visitor profile. The visitor's IP (from cf-connecting-ip) is used for rate-limiting only and is not written to storage.
Beyond the browser. The context model is open-ended — anything that helps describe who the visitor is and what they need can be passed on the request. The client-side signals above are the zero-setup default, but an integrator can enrich them with consented, first-party data:
Adobe profile
AEP & Real-Time CDP
The Real-Time Customer Profile from Adobe Experience Platform — audiences, computed attributes and edge-resolved identity.
Known customer
CRM & account
Lifecycle stage, tier, entitlements and purchase history for a signed-in visitor.
Loyalty
Membership & rewards
Points balance, status and benefits that change what's worth surfacing.
Commerce
Catalog & cart
Live stock, pricing, active promotions and the current cart or basket state.
Environment
Place & time
Geo, locale, weather or nearest-store inventory for context-aware framing.
Identity
Consented identity
A first-party identity graph that ties sessions together — with the visitor's consent.
A natural fit with Adobe Experience Platform. Because of1 is Adobe technology, connecting it to the Real-Time Customer Profile in Adobe Experience Platform / Real-Time CDP is a natural extension. An edge or server integration can resolve the visitor's profile — audiences, computed attributes and propensity scores — and pass it into the generation request, so the page reasons over the same unified profile the rest of your Adobe stack already uses. No built-in connector is required: any profile data that describes intent can be handed in on the request.
Still your data, still stateless. These richer sources are opt-in and first-party — supplied by you, with consent, as part of the request. of1 itself stays stateless: it reasons over whatever context it is handed for that one request and keeps nothing afterward. Any signal that helps describe intent can be plugged in.
What is stored
The service is stateless per request. It persists your content configuration — not visitor data.
- R2 object storage — Tenant configs (products, personas, features, FAQs, templates, brand voice). Static, brand-level — not request-specific.
- Vectorize index — Embeddings of your products / features / FAQs. Content only, scoped per tenant; no request metadata.
- KV cache — Loaded configs (300s), suggestions (300s), governance rules (3600s). Short-lived caches, keyed by tenant.
Stateless by design. /api/generate does not log individual requests or responses to persistent storage. Behavioural analysis is computed per request and discarded. Multi-turn context works because the caller tracks and re-sends the behaviour profile — of1 keeps nothing between calls. Each tenant's configs, vectors and cache keys are fully isolated.
API endpoints
Public endpoints are CORS-enabled (Access-Control-Allow-Origin: *) and need no auth. Generation is rate-limited to 30 requests / 60s per IP. Admin endpoints require a token.
POST /api/generate
Generate a page — streams NDJSON. A minimal generation request:
{
··"id": "frescopa--of1-demo--aem-growth-adoption",
··"query": "recommend a coffee machine for me",
··"browsing": [{ "url": "/espresso", "dwellTimeMs": 8200, "scrollDepth": 0.7 }],
··"segment": { "device": "mobile", "region": "ch" }
}
The response is a stream of NDJSON lines — section, suggestions, debug, done (default flow) or a single page event with full HTML + stylesheet (template-routing flow) — which the client consumes as it arrives. Per-request overrides such as ?device=mobile®ion=fr&provider=bedrock&model=claude-opus are supported.
POST /api/suggest
Follow-up exploration prompts.
POST /api/personalize
Rewrite existing page elements per behaviour.
POST /api/tenants/:id/sync
Pull & index tenant config from EDS (admin).
Embed the SDK
The client SDK handles streaming, DOM injection and the debug widget so you don't have to.
<!-- 1. Load the SDK -->
<link rel="stylesheet" href="https://of1-gen-web-service.franklin-prod.workers.dev/sdk/of1-client.css">
<script src="https://of1-gen-web-service.franklin-prod.workers.dev/sdk/of1-client.js"></script>
// 2. Initialise against a container
OF1.init(document.querySelector('#of1'), {
··id: 'frescopa--of1-demo--aem-growth-adoption',
··query: userQuery,
});
Debug mode. Append ?debug=1 to any page using the SDK to reveal a widget showing the model used, timings, intent classification, RAG match counts and token usage — plus a runtime model-override control (stored in sessionStorage).
Tenant configuration
A tenant is your brand. Its content and behaviour are defined by JSON files authored in AEM / EDS and pulled into of1 via sync.
brand-voice.json— Personality, tone, vocabulary, words to avoidproducts.json— Products — id, name, category, price, keywords, highlights, imagespersonas.json— Personas and their keywords / preferencesuse-cases.json— Use-cases matched against the queryfeatures.json,faqs.json— Feature descriptions and Q&A used in RAGgenerative-images.json,generative-fragments.json— Approved images and fragments for levels 1 and 2, with their collections and typestemplates.json/block-guide.json— Template routing definitions, or block layout rulessuggestions.json,cta-template.json— UI strings and the CTA mustache template
Publish these at https://<id>.aem.page/of1/config/<file>.json, then call POST /api/tenants/<id>/sync (with the admin token) to pull them into R2 and rebuild the vector index. Configs are cached in KV for 300 seconds.
Models & infrastructure
- Page generation & intent classification — Cerebras
gpt-oss-120b(default; model-agnostic — GPT, Claude, Llama, Mistral, Bedrock) - Template selection & suggestions — Cloudflare Workers AI
@cf/meta/llama-3.1-8b-instruct - Embeddings for RAG — Cloudflare AI
@cf/baai/bge-base-en-v1.5 - Compute — Cloudflare Workers at the edge
- Storage — R2 (configs), Vectorize (embeddings), KV (cache)
- Brand governance — Adobe Brand Governance agent (optional; colours, tone, guardrails per domain / segment)
Model-agnostic. The reasoning engine is not tied to one provider. Providers are adapters — Cerebras, Workers AI and Bedrock ship today — and the model can be overridden per request for testing.
of1 · generative websites for an audience of one. This page documents OF1 Product — the runtime that produces personalized pages (of1-gen-web-service). © 2026 Adobe — early-stage technology by the Adobe Experience Manager team.