Developer Demo
Optimizely One Platform
Key Things to Know#
- →Graph is the only read API. CMS content, DAM asset metadata and external systems registered through the Content Source API all converge on it, so the app has one query language and one cache story instead of one integration per product.
- →Experimentation happens before rendering. FX decides in edge middleware and the variation is encoded into the URL as
__v_flag--variation, which gives every bucket its own stable ISR cache entry. Personalised pages stay statically cached instead of going dynamic. - →Client-side tools can still reach server-rendered output. Web Experimentation runs in the browser but writes a cookie that middleware reads on the next request, so the variation survives into the server render with no flash.
- →ODP closes the loop. Behavioural events go out, audience membership comes back, and a segment is mapped to a Graph variation key. Data collected on one request changes the content served on the next.
- →Two of these products are not integrated here. Content Recommendations and Product Recommendations are drawn as reference boxes, and Mark AI is authoring-side only. See the status table.
Architecture Diagram #
Content supply on the left, Graph as the hub, the Next.js app in the middle split by runtime, and the decisioning and data products on the right. Solid boxes are wired up in this repo; dashed boxes are reference architecture.
The Three Feedback Loops #
Boxes and arrows show what talks to what. These loops are the reason the stack is worth composing: each one carries a signal from one product back into another and changes what the next request renders.
Data to decision to content
Behaviour observed in the browser becomes audience membership in ODP, which becomes a variation key in a Graph query. The content served changes because of what the visitor did earlier.
mb_* event
AutoTracker
ODP profile
zaius tag
Audience
/v3/graphql
variation key
resolveVariationKey()
Graph filter
includeOriginal
Personalised page
RSC render
Publish to invalidate
Nothing polls. An editor publishing is what makes the cache drop, and the one-hour TTL is only the ceiling if a webhook is ever missed.
Editor publishes
Visual Builder
Graph reindexes
~30-60s
Webhook
doc.updated
revalidateTag
every CACHE_TAG
Next request
rerenders once
Client decision to server render
The loop that makes a client-side tool safe on a statically cached site. WX picks its variation in the browser, but the visible render on the next navigation is server-side, so there is no flash of the original.
WX snippet
browser
opti_wx_variation
cookie
Middleware
validates vs datafile
__v_ segment
URL rewrite
Cached variant
own ISR entry
Product by Product #
What each product does in this architecture, how it is actually connected, and where that connection lives in the code.
Optimizely Graph
Wired upThe single read API. Every content-shaped thing in the stack converges here: CMS pages, DAM asset metadata, and external systems registered through the Content Source API.
Server-side GraphQL over the cms-sdk GraphClient against cg.optimizely.com, single-key auth. Cached queries are "use cache" functions wrapping request() with a cacheTag; unbounded-input queries (search, autocomplete, geo) call request() directly and stay uncached.
- src/lib/optimizely/graphClient.ts
- src/lib/graphql/queries/
SaaS CMS
Wired upAuthoring, Visual Builder compositions, display templates, preview and the editorial workflow. Publishes sync into Graph rather than being read directly by the app.
Content types pushed from code via opti:push. Reads go through Graph; writes (seeding, migrations) go through the Management API with an OAuth client-credentials token. Preview uses a short-lived token, or App Key Basic auth for shareable external links.
- optimizely.config.mjs
- src/lib/optimizely/componentRegistry.ts
- scripts/_shared.ts
Feature Experimentation
Wired upFlags, A/B tests and server-side bucketing. The decision that matters most is made before rendering, in middleware, so each variation can be cached independently.
Datafile-only SDK (60s TTL) running in three places: edge middleware (decideAll, encoded into the URL), server components via getOptimizelyUser(), and the browser for impressions. Impressions are suppressed by default and fired once by the component that actually renders the variant.
- src/middleware.ts
- src/lib/optimizely/user.ts
- src/lib/optimizely/variationPath.ts
Data Platform (ODP)
Wired upThe behavioural profile store. Events flow out of the app, come back as audience membership, and are mapped to a Graph variation key so the next render is personalised.
Three channels: the client zaius tag for pageviews and mb_* events, a server POST to /v3/events for form submissions, and a server /v3/graphql query for audience membership (5 min cache). Segments map to variation keys through ODP_SEGMENT_TO_VARIATION.
- src/lib/optimizely/odp.ts
- src/components/OdpSetup.tsx
- src/components/AutoTracker.tsx
Web Experimentation
Wired upVisual, marketer-owned client-side testing on top of the same visitor identity as FX. Useful for changes that do not need a code deploy.
A blocking snippet in <head> (sync on purpose, to avoid a flash of the original). A WX custom-JS action writes an opti_wx_variation cookie, which middleware validates against the FX datafile and folds into the URL segment, so the next request is server-rendered. FX decisions take precedence.
- src/app/layout.tsx
- src/middleware.ts
DAM / CMP Assets
Wired upAsset library and delivery. Images arrive as references on content and are rendered with responsive srcsets and on-the-fly CDN resizing.
Delivery-side only: asset metadata comes through Graph on the content reference, binaries come from the CMP CDN with resize params, and an allow-listed proxy route adds long-lived caching. There are no direct DAM REST calls.
- src/lib/optimizely/damImage.ts
- src/app/api/image-proxy/route.ts
Content Source API
Wired upBrings systems that are not the CMS into Graph, so a PIM, a branch database or a quote feed can be queried with the same GraphQL and the same facets as CMS content.
Build and seed time: a schema is PUT to the content source endpoint, then rows are pushed as NdJSON under App Key Basic auth. Once indexed, the items read back exactly like CMS content, including GeoPoint geo search.
- scripts/_contentSource.ts
- src/lib/graphql/queries/GetLocations.ts
Mark AI
PartialAI agents over the stack: content generation and review, SEO and GEO analysis, and natural-language authoring against the CMS.
Present as documentation plus a dev-time MCP connection (the CMS MCP server, and an experimentation MCP server in .mcp.json) that a developer or editor drives from their tooling. There is no runtime call from the app itself, so this is an authoring-side accelerator rather than a request-path dependency.
- src/app/demo/mark-ai/page.tsx
- src/app/demo/mcp-server/page.tsx
- .mcp.json
Content Recommendations
ReferencePicks the next best article or page per visitor from their behavioural profile, typically for a "recommended for you" rail.
Not integrated here. Wiring it up means adding the recs script or API, letting it read the ODP profile keyed on the same optimizelyEndUserId cookie, and resolving the returned content keys through Graph so the rail renders with the existing block components.
Product Recommendations
ReferenceCatalog-driven merchandising: also-bought, trending and personalised product rails.
Not integrated here, and it needs something this demo does not have: a product catalog feed. The natural fit would be to register the catalog through the Content Source API so products are queryable in Graph, then feed behavioural signals from the existing mb_* event layer.
Request Lifecycle #
One request for a CMS page, from the edge to the rendered HTML, plus the invalidation that follows a publish. This is the path the diagram traces.
- 1
Edge middleware
Mints or reads the
optimizelyEndUserIdcookie, applies CMS-managed redirects, runs FXdecideAllagainst the cached datafile, keeps only flags marked as CMS flags whose route matches, and rewrites the URL with up to three__v_flag--variationsegments. The WX cookie is folded in here too, only where FX has no decision for that flag. - 2
Catch-all route
extractVariations()splits the variation segments back off the slug, thenbuildUrlCandidates()produces the locale-aware URL candidates to try. On the homepage only, with no FX variation, the ODP segment read runs and the route opts out of caching - which is why/is dynamic and every other route stays on ISR. - 3
Graph query
getContentByPath()runs per candidate with a variation filter that always setsincludeOriginal: true, so a visitor who matches no variation still gets the original page. A key-lookup fallback covers the case where the path query misses. - 4
Render
The component registry dispatches the returned type to an experience or page component, which walks the composition tree and renders each block. The variation actually served is reported back to FX as an impression from the client.
- 5
Invalidate
An editor publishes, Graph reindexes and fires its webhook, and the webhook route calls
revalidateTagfor every tag inCACHE_TAGS. The one-hour TTL is only the fallback ceiling.
What Is Actually Wired Up #
The diagram is a reference architecture, so it includes products this demo does not use. This table is the honest version: what you can go and read the code for today.
| Product | Status | In this repo |
|---|---|---|
| Optimizely Graph | Wired up | Core read path, 19 named queries |
| SaaS CMS | Wired up | Management API, preview, Visual Builder |
| Feature Experimentation | Wired up | Middleware, server and browser runtimes |
| Data Platform (ODP) | Wired up | zaius tag, /v3/events, /v3/graphql segments |
| Web Experimentation | Wired up | Blocking snippet plus cookie bridge |
| DAM / CMP | Wired up | Delivery side only, no DAM REST calls |
| Content Source API | Wired up | Seed time: quotes and branch locations |
| Mark AI | Partial | Docs and dev-time MCP, no runtime integration |
| Content Recommendations | Reference | Not integrated |
| Product Recommendations | Reference | Not integrated |