Experience Decisioning

Adobe Journey Optimizer Code‑Based Experiences on an Edge Delivery Services Site

Code‑Based Offers on an Edge Delivery Services guide

This guide shows how to connect Adobe Journey Optimizer (AJO) Code‑Based Experiences to an Edge Delivery Services (EDS) page

Configuring AJO Code‑Based Offers on an Edge Delivery Services Page

This guide shows how to connect Adobe Journey Optimizer (AJO) Code‑Based Experiences to an Edge Delivery Services (EDS) page so that:

All examples use this lab page, where the personalized offers appear below the hero:

https://edgepatterns.dev/labs/ajo-code-based-experience-eds

The surface below the hero is:

web://edgepatterns.dev/labs/ajo-code-based-experience-eds#offers-below-hero

1. Authoring: define the offers block and surface name

Authors work in document authoring (Docs/Word/SharePoint) and create an offers block using a table.

The first header cell defines the block type (offers).
The second header cell defines a surface key, which becomes part of the surface URI (for example, offers-below-hero).

Example authoring table:

After the EDS pipeline processes this, the relevant HTML looks like:

<div class="offers offers-below-hero">
  <!-- block content -->
</div>

Here:

web://edgepatterns.dev/labs/ajo-code-based-experience-eds#offers-below-hero

You can repeat this pattern for more slots (e.g., offers-sidebar, offers-footer) by changing the value in brackets.

2. Adobe Tags: discover surfaces and call AJO

Next, use Adobe Tags with the AEP Web SDK to:

  1. Discover all offers blocks on the page.
  2. Derive their full surface URIs.
  3. Call alloy("sendEvent") once with all surfaces.
  4. Broadcast the decisions to the page via a custom event.

2.1 Data Element: “AJO Surfaces”

Create a Custom Code Data Element, for example named AJO Surfaces, that:

Example implementation:

const base = 'web://edgepatterns.dev';
const path = window.location.pathname;
const surfaces = [];

document.querySelectorAll('.offers').forEach((block) => {
  const classes = Array.from(block.classList);
  // Any class that is not "offers" is treated as the surface key
  const key = classes.find((cls) => cls !== 'offers'); // e.g. "offers-below-hero"
  if (!key) return;

  const fullSurface = `${base}${path}#${key}`;
  if (!surfaces.includes(fullSurface)) {
    surfaces.push(fullSurface);
  }
});

return surfaces;

On the lab page above, this will return:

[
  "web://edgepatterns.dev/labs/ajo-code-based-experience-eds#offers-below-hero"
]

If you add more offers blocks with different second classes, they will automatically be included in this array.

Note

This can be either done by Custom code via alloy(sendEvent.. or sending propositionFetch via UI and listening Send Event Complete event

2.2 Rule: call AJO at page top

Create a rule such as “Personalization – Page top”:

From here on, the page will receive a single personalization event per load, with decisions for each surface.

2.3 Rule: Send Event Complete

Create rule where you listen WebSDK Send Event Complete and do the following custom code - here we check if propositionFetch was completed and we fire new custom event called aep:personalization which will be picked up by our offers.js code

if (!Object.prototype.hasOwnProperty.call(event, 'decisions')) {
  return;
}

var decisions = Array.isArray(event.decisions) ? event.decisions : [];

window.dispatchEvent(new CustomEvent('aep:personalization', {
  detail: { decisions: decisions }
}));

3. AJO: configure the Code‑Based Experience and surfaces

On the Journey Optimizer side, you need:

  1. A Code‑Based channel configuration with matching surfaces.
  2. A Code‑Based Experience that emits JSON.
  3. Experience Decisioning policy to select offers.

This follows the model described in the “Code‑based experience”, “Configure code‑based channel”, “Get started with code‑based experiences”, and “Code‑based experience prerequisites” documentation on Experience League.

3.1 Code‑Based channel and surfaces

  1. Go to Administration → Channels → Code‑based channel.

  2. Create a Web configuration.

  3. Define surfaces that match what your page sends. For the lab page:

    web://edgepatterns.dev/labs/ajo-code-based-experience-eds#offers-below-hero
    

The string must match exactly what Tags builds from the block classes and the URL path.

You can optionally define wildcard surfaces (for example, wildcard:web://edgepatterns.dev/*#offers-below-hero) if you want to reuse the same slot across multiple pages.

3.2 Code‑Based Experience with JSON output

Create a Code‑Based Experience (campaign or journey):

  1. Channel configuration: select the Code‑Based channel where you defined your web surfaces.

  2. Content format: choose JSON so the Edge returns structured data instead of HTML.

  3. Targeting / Experience Decisioning:

    • Optionally attach an Experience Decisioning policy to the surface to select offers based on profile, context, or ML ranking.
    • Limit to three items if you want exactly three offers below the hero.

Shape your template so that, when the Edge responds to the Web SDK sendEvent, the payload looks like this:

{
  "decisions": [
    {
      "scope": "web://edgepatterns.dev/labs/ajo-code-based-experience-eds#offers-below-hero",
      "items": [
        {
          "id": "offer-1",
          "data": {
            "imageUrl": "https://example.com/img1.jpg",
            "ctaUrl": "https://example.com/campaign-1",
            "title": "Offer One",
            "ctaText": "Learn more"
          }
        },
        {
          "id": "offer-2",
          "data": {
            "imageUrl": "https://example.com/img2.jpg",
            "ctaUrl": "https://example.com/campaign-2",
            "title": "Offer Two",
            "ctaText": "Learn more"
          }
        },
        {
          "id": "offer-3",
          "data": {
            "imageUrl": "https://example.com/img3.jpg",
            "ctaUrl": "https://example.com/campaign-3",
            "title": "Offer Three",
            "ctaText": "Learn more"
          }
        }
      ]
    }
  ]
}

For JSON authoring in Code‑Based Experiences, you can follow the patterns described in “Delivering Personalization with JSON Content in Adobe Journey Optimizer” and “Create code‑based experiences”.

The key contract for offers.js is:

4. EDS: implement offers.js to render offers

You can find the offers.js implementation used on this page here https://github.com/samircaus/da-elsie/blob/main/blocks/offers/offers.js

The EDS offers block is a surface-based, event-driven component. The block’s only job is to detect that surface, load the right renderer, reserve space, and then listen once for the aep:personalization aep:personalization custom event and delegate rendering to the surface-specific renderer.

Surface resolution and renderer loading

On decorate(block), the code derives the surface with getSurfaceNameFromBlock(block) by taking the first class that is neither offers nor block. That name is used to look up a loader in a RENDERERS map (e.g. offers-below-hero → dynamic import of ./renderers/offers-below-hero.js). The block loads the renderer’s CSS via getRendererCssUrl(surfaceName) (same origin as the script) and then dynamically imports the renderer module. If the surface is unknown or loading fails, the block logs and exits without rendering.

Personalization event and data mapping

The block builds a surface URI as web://edgepatterns.dev + current path + # + surface name, and subscribes to aep:personalization with { once: true }. When the event fires, it uses getDecisionsForSurface(event.detail, surfaceUri) to get decisions for that surface, preferring detail.decisions and falling back to detail.propositions, both filtered by scope === surfaceUri. The first matching decision’s items are normalized with mapDecisionItemsToOffers() into a uniform shape (id, title, description, imageUrl, ctaText, ctaUrl), supporting both item.data and flat item structures. If there are offers, the block calls renderer.render(block, offers, {}).

Flow summary

The block clears the element, optionally calls renderer.reserveHeight(block) to avoid layout shift, and adds offers--ready. It then waits for the first aep:personalization event, extracts and maps offers for the current surface, and hands them to the renderer. So the logic is: decorate → resolve surface → load renderer (and CSS) → reserve height → listen once for personalization → map event data to offers → render.

  1. Derives the surface key from the block’s classes.
  2. Builds the full surface URI the same way as the Data Element.
  3. Renders the returned offers into the block.

This implementation assumes:

If you later introduce different rendering “types”, you can:

5. Recap and extension ideas

With this setup you get:

From here, you can extend the same pattern to additional slots:

This approach builds directly on the patterns described in the Journey Optimizer “Code‑based experience”, “Configure code‑based channel”, and “Get started with code‑based experiences” documentation, as well as the AEP Web SDK “Using Adobe Journey Optimizer with the Experience Platform Web SDK” and “personalization in sendEvent” guidance, and fits naturally into the Edge Delivery Services block model.