> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aftersell.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Brugerdefineret integration

> Kald Strategies API direkte for at drive brugerdefinerede butiksfacade-oplevelser - PDP-karruseller, brugerdefinerede kurv-upsells og alle andre steder, hvor Aftersells egne flader ikke når ud.

<div id="overview">
  ## Oversigt
</div>

Når hverken Aftersells egne flader (post-purchase, checkout, Upcart) eller en pakket integration passer, kan du selv kalde Strategies API fra dit Shopify-tema og vise de returnerede produkter, som du vil.

Mønsteret er det samme i alle tilfælde: byg en kontekst-payload fra Liquid (så Shopify-attributter som det aktuelle produkt, kurvens indhold og kundefelter udfyldes ved rendering), `POST` den til `/api/public/strategy/evaluate`, og vis svaret.

Denne side dækker to implementeringsmønstre:

* **PDP-kontekst** - placér en sektion på produktsider, der kalder API'en med det **aktuelt viste produkt** og viser en karrusel med returnerede anbefalinger.
* **Kurvkontekst** - vis en upsell-blok i en brugerdefineret kurv, der kalder API'en med **alle aktuelle kurvvarelinjer** og viser de returnerede produkter.

Formen på **produktkonteksten** er det, der adskiller de to: et enkelt produkt på PDP, et array med alle varelinjer i kurven.

***

<div id="what-youll-need">
  ## Hvad du skal bruge
</div>

1. **Din Strategy API-nøgle.** I Aftersell skal du gå til **Settings → Product Strategy** og i kortet **Security Token** kopiere dit token (dette er din Strategy API-nøgle).
2. **Strategy-ID'et.** Åbn den Strategy, du vil køre, i Aftersells Strategy-editor, og kopiér dens ID.
3. **Adgang til temakode.** Du skal tilføje en Liquid-sektion (PDP) eller blok (brugerdefineret kurv) til dit Shopify-tema - Online Store → Themes → ... → Edit code.

<Warning>
  Din Strategy API-nøgle ligger i klientside-temakode, hvilket gør den synlig for alle, der ser sidens kildekode. Behandl den som en offentlig butiksfacade-legitimation, og rotér den fra Aftersell **Settings → Product Strategy**, hvis den nogensinde eksponeres på en måde, du ikke havde til hensigt.
</Warning>

***

<div id="pdp-context-section-snippet">
  ## PDP-kontekst: Sektionssnippet
</div>

Dette mønster tilføjer en Shopify-sektion til din produktside. Når siden renderes, indlejrer Liquid det aktuelle produkts, kurvens og kundens attributter i payloaden, hvorefter JavaScript poster til Strategies API og viser de returnerede produkter i en Splide-karrusel.

<div id="installing">
  ### Installation
</div>

1. I din Shopify-administrator skal du gå til **Online Store → Themes**, klikke på **...** på dit tema og vælge **Edit code**.
2. Under mappen **Sections** skal du oprette en ny fil med navnet `aftersell-upsell-carousel.liquid`.
3. Indsæt snippet nedenfor i den nye fil, og erstat `YOUR_STRATEGY_API_KEY` med API-nøglen fra Aftersell.
4. Gem.
5. Åbn din produktskabelon (typisk `templates/product.json` eller `sections/main-product.liquid`), og tilføj sektionen **Aftersell Carousel** der, hvor karrusellen skal vises. Fra temaeditoren kan du også trække den direkte ind på produktsiden.
6. I sektionens indstillinger skal du indsætte dit **Strategy-ID**.

<div id="what-the-section-sends">
  ### Hvad sektionen sender
</div>

For hver PDP-visning indeholder payloaden:

* **products** - et array med ét element, der indeholder det aktuelt viste produkt (productId, variantId, quantity, price, handle, title, vendor, productType, tags, collections, sellingPlan).
* **cart** - subtotal, antal varer, antal linjer i kundens aktuelle kurv (udelades, hvis kurven er tom).
* **cartToken** - så API'en kan knytte denne evaluering til samme session.
* **customer** - tags, land, provins, sprogindstilling, ordreantal, samlet forbrug og accepterer-marketing-flag, men **kun hvis kunden er logget ind**.
* **session** - valutakode fra `shop.currency`.

Sektionen sender ikke UTM-parametre som standard. Hvis du vil have UTM-baseret målretning på PDP, skal du registrere dem på klientsiden og tilføje dem til `session`-objektet før fetch-kaldet.

<div id="the-snippet">
  ### Snippet
</div>

```liquid theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
{% comment %}
  Aftersell Carousel (Splide)
  Type: Section — save to sections/aftersell-upsell-carousel.liquid
{% endcomment %}

{% if product %}

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/css/splide-core.min.css">
<script src="https://cdn.jsdelivr.net/npm/@splidejs/splide@4.1.4/dist/js/splide.min.js" defer></script>

<div id="aftersell-upsell-{{ section.id }}" class="aftersell-upsell-carousel" style="display:none" {{ section.shopify_attributes }}>
  <h2 class="aftersell-heading">{{ section.settings.heading | default: 'You might also like' }}</h2>
  <div class="aftersell-carousel-wrapper">
    <button class="aftersell-arrow aftersell-arrow--prev" aria-label="Previous" disabled>&#8592;</button>
    <div class="aftersell-track-container">
      <div class="splide" id="aftersell-splide-{{ section.id }}">
        <div class="splide__track">
          <ul class="splide__list">
            <li class="splide__slide"><div class="aftersell-skeleton"></div></li>
            <li class="splide__slide"><div class="aftersell-skeleton"></div></li>
            <li class="splide__slide"><div class="aftersell-skeleton"></div></li>
            <li class="splide__slide"><div class="aftersell-skeleton"></div></li>
          </ul>
        </div>
      </div>
    </div>
    <button class="aftersell-arrow aftersell-arrow--next" aria-label="Next" disabled>&#8594;</button>
  </div>
</div>

<style>
.aftersell-upsell-carousel { font-family: Modernist, sans-serif; max-width: 1300px; margin: 0 auto; padding: 24px 0 0; box-sizing: border-box; }
.aftersell-heading { font-family: Modernist, sans-serif; font-size: 30px; font-weight: 700; line-height: 45px; color: #0C0A09; margin: 0; }
.aftersell-carousel-wrapper { display: flex; align-items: center; gap: 8px; }
.aftersell-track-container { overflow: hidden; flex: 1; min-width: 0; }
.aftersell-card { display: flex; flex-direction: column; box-sizing: border-box; height: 100%; }
.aftersell-card-link { text-decoration: none; color: inherit; display: block; flex: 1; }
.aftersell-card-image { aspect-ratio: 1; overflow: hidden; background: #f5f5f5; border-radius: 8px 8px 0 0; position: relative; }
.aftersell-card-image img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.3s ease; }
.aftersell-card-image:hover img { transform: scale(1.04); }
.aftersell-card-badge { position: absolute; top: 10px; left: 8px; background: #c60006; color: #fff; font-size: 11px; font-weight: 500; padding: 4px 8px; border-radius: 16px; z-index: 1; }
.aftersell-card-body { padding: 6px 0 0; display: flex; flex-direction: column; }
.aftersell-card-vendor { font-size: 13px; font-weight: 550; text-transform: uppercase; color: #1d4481; margin: 0; }
.aftersell-card-title { font-size: 13px; font-weight: 700; color: #0C0A09; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.aftersell-card-price { font-size: 13px; color: #0C0A09; margin: 0; display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.aftersell-card-price--compare { text-decoration: line-through; color: #595959; }
.aftersell-card-price--sale { font-weight: 700; color: #c60006; }
.aftersell-card-actions { padding: 6px 0 0; display: flex; flex-direction: column; margin-top: auto; }
.aftersell-variant-select { width: 100%; font-size: 13px; padding: 0.35rem 0.5rem; border: 1px solid #DBDBDB; border-radius: 4px; background: #fff; color: #0C0A09; cursor: pointer; }
.aftersell-cta { padding: 16px 12px; background: #c50007; color: #FFFFFF; border: none; border-radius: 5px; font-size: 16px; font-weight: 700; cursor: pointer; width: 100%; display: flex; align-items: center; justify-content: center; transition: background 0.2s, opacity 0.2s; }
.aftersell-cta:hover:not(:disabled) { opacity: 0.85; }
.aftersell-cta:disabled { opacity: 0.5; cursor: default; }
.aftersell-cta--added { background: #1d4481; }
.aftersell-cta--unavailable { background: #999; cursor: default; }
.aftersell-arrow { width: 30px; height: 90px; background: rgba(255,255,255,0.6); border: 1px solid #DBDBDB; border-radius: 5px; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; padding: 0; }
.aftersell-arrow:disabled { opacity: 0.3; cursor: default; }
.aftersell-skeleton { border-radius: 8px; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: aftersell-shimmer 1.4s infinite; aspect-ratio: 0.75; width: 100%; }
@keyframes aftersell-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }

#aftersell-splide-{{ section.id }} .splide__arrows,
#aftersell-splide-{{ section.id }} .splide__pagination { display: none !important; }

@media (max-width: 768px) {
  .aftersell-upsell-carousel { padding: 16px 16px 0; }
  .aftersell-arrow { display: none; }
  .aftersell-carousel-wrapper { gap: 0; }
}
</style>

<script>
(function() {
  var BACKEND_URL  = 'https://start.aftersell.app';
  var API_KEY      = 'YOUR_STRATEGY_API_KEY';
  var STRATEGY_ID  = {{ section.settings.strategy_id | json }};
  var SHOP_DOMAIN  = {{ shop.permanent_domain | json }};
  var CTA_LABEL    = '{{ section.settings.cta_label | default: "Add to cart" }}';
  var MAX_PRODUCTS = {{ section.settings.max_products | default: 8 }};
  var CURRENCY     = {{ shop.currency | default: "USD" | json }};
  var SECTION_ID   = {{ section.id | json }};
  if (!SHOP_DOMAIN || !STRATEGY_ID) return;

  var fmt;
  try {
    fmt = new Intl.NumberFormat('en-US', { style: 'currency', currency: CURRENCY });
  } catch(e) {
    fmt = { format: function(n) { return '$' + parseFloat(n).toFixed(2); } };
  }
  function money(str) { return fmt.format(parseFloat(str) || 0); }

  function gidToNumeric(gid) {
    return gid ? String(gid).split('/').pop() : null;
  }

  function priceHtml(price, compareAt) {
    var hasSale = compareAt && parseFloat(compareAt) > parseFloat(price);
    return hasSale
      ? '<span class="aftersell-card-price--compare">' + money(compareAt) + '</span>'
        + '<span class="aftersell-card-price--sale">' + money(price) + '</span>'
      : '<span class="aftersell-card-price--current">' + money(price) + '</span>';
  }

  var productContext = {
    productId:   'gid://shopify/Product/{{ product.id }}',
    variantId:   'gid://shopify/ProductVariant/{{ product.selected_or_first_available_variant.id }}',
    quantity:    1,
    price:       {{ product.price | divided_by: 100.0 }},
    handle:      {{ product.handle | json }},
    title:       {{ product.title | json }},
    vendor:      {{ product.vendor | json }},
    productType: {{ product.type | json }},
    tags:        {{ product.tags | json }},
    collections: [{% for col in product.collections %}'gid://shopify/Collection/{{ col.id }}'{% unless forloop.last %},{% endunless %}{% endfor %}],
    sellingPlan: {% if product.selected_selling_plan %}'subscription'{% else %}'one-time'{% endif %}
  };

  var cartContext = {
    subtotal:  {{ cart.total_price | divided_by: 100.0 }},
    itemCount: {{ cart.item_count }},
    lineCount: {{ cart.items.size }}
  };

  {% if customer %}
  var customerContext = {
    customerId:       'gid://shopify/Customer/{{ customer.id }}',
    tags:             {{ customer.tags | json }},
    {% if customer.default_address.country_code %}countryCode: {{ customer.default_address.country_code | json }},{% endif %}
    {% if customer.default_address.province_code %}provinceCode: {{ customer.default_address.province_code | json }},{% endif %}
    locale:           {{ request.locale.iso_code | json }},
    orderCount:       {{ customer.orders_count }},
    totalSpent:       {{ customer.total_spent | times: 1.0 }},
    acceptsMarketing: {{ customer.accepts_marketing }}
  };
  {% endif %}

  var blockEl  = document.getElementById('aftersell-upsell-' + SECTION_ID);
  var splideEl = document.getElementById('aftersell-splide-' + SECTION_ID);
  var list     = splideEl.querySelector('.splide__list');
  var prevBtn  = blockEl.querySelector('.aftersell-arrow--prev');
  var nextBtn  = blockEl.querySelector('.aftersell-arrow--next');
  var splideInstance = null;

  function getCartToken() {
    var t = {{ cart.token | json }};
    if (t) return Promise.resolve(t);
    return fetch('/cart.js').then(function(r){ return r.json(); }).then(function(c){ return c.token || null; }).catch(function(){ return null; });
  }

  function evaluate() {
    getCartToken().then(function(cartToken) {
      if (!cartToken) return;
      return fetch(BACKEND_URL + '/api/public/strategy/evaluate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Strategy-Api-Key': API_KEY },
        body: JSON.stringify({
          shopDomain:  SHOP_DOMAIN,
          strategyId:  STRATEGY_ID,
          context: {
            products:  [productContext],
            cartToken: cartToken,
            cart:      cartContext.itemCount > 0 ? cartContext : undefined,
            {% if customer %}customer: customerContext,{% endif %}
            session:   { currencyCode: CURRENCY }
          }
        })
      });
    }).then(function(res) { if (!res) return; return res.json(); })
    .then(function(data) {
      if (!data || !data.success || !data.products || !data.products.length) {
        clearSkeletons(); return;
      }
      renderCards(data.products.slice(0, MAX_PRODUCTS));
    }).catch(function(err) {
      console.error('[AfterSell carousel] Error:', err);
      clearSkeletons();
    });
  }

  function renderCards(products) {
    if (!products || !products.length) {
      blockEl.style.display = 'none'; return;
    }

    blockEl.style.display = '';

    list.innerHTML = products.map(function(p, i) {
      var img      = p.images && p.images[0] ? p.images[0].src : '';
      var alt      = (p.images && p.images[0] && p.images[0].altText) || p.title;
      var variants = p.variants || [];
      var availableVariants = variants.filter(function(v) {
        return v.availableForSale !== false;
      });
      var isDefaultVariant = variants.length === 1 && variants[0].title === 'Default Title';
      var showSelect       = !isDefaultVariant && availableVariants.length >= 1;
      var firstVariant     = availableVariants.length ? availableVariants[0] : null;
      var firstVariantId   = firstVariant ? gidToNumeric(firstVariant.variantId) : null;
      var anyAvailable     = availableVariants.length > 0;

      var onSale = p.compareAtPrice && parseFloat(p.compareAtPrice) > parseFloat(p.price);
      var badge  = '';
      if (onSale) {
        var pct = Math.round((1 - parseFloat(p.price) / parseFloat(p.compareAtPrice)) * 100);
        badge = '<span class="aftersell-card-badge">Save ' + pct + '%</span>';
      }
      var vendor = p.vendor ? '<p class="aftersell-card-vendor">' + p.vendor + '</p>' : '';

      var selectHtml = '';
      if (showSelect) {
        var options = availableVariants.map(function(v) {
          return '<option value="' + gidToNumeric(v.variantId) + '"'
            + ' data-price="'   + (v.price         || p.price) + '"'
            + ' data-compare="' + (v.compareAtPrice || '')     + '"'
            + '>' + v.title + '</option>';
        }).join('');
        selectHtml = '<select class="aftersell-variant-select" aria-label="Select variant">' + options + '</select>';
      }

      var atcLabel = anyAvailable ? CTA_LABEL : 'Sold Out';
      var atcClass = anyAvailable ? 'aftersell-cta' : 'aftersell-cta aftersell-cta--unavailable';
      var atcBtn   = '<button class="' + atcClass + '"'
        + (firstVariantId ? ' data-variant-id="' + firstVariantId + '"' : '')
        + (anyAvailable ? '' : ' disabled')
        + '>' + atcLabel + '</button>';

      var productUrl = p.url + (p.url.indexOf('?') > -1 ? '&' : '?') + 'ref=aftersell';

      return '<li class="splide__slide">'
        + '<div class="aftersell-card" data-idx="' + i + '">'
        + '<a class="aftersell-card-link" href="' + productUrl + '">'
        + '<div class="aftersell-card-image">' + badge
        + (img ? '<img src="' + img + '" alt="' + alt + '" loading="lazy">' : '')
        + '</div>'
        + '<div class="aftersell-card-body">'
        + vendor
        + '<p class="aftersell-card-title">' + p.title + '</p>'
        + '<p class="aftersell-card-price" data-price-el>' + priceHtml(p.price, p.compareAtPrice) + '</p>'
        + '</div></a>'
        + '<div class="aftersell-card-actions">'
        + selectHtml
        + atcBtn
        + '</div></div></li>';
    }).join('');

    attachCardEvents();
    initSplide();
  }

  function attachCardEvents() {
    list.addEventListener('change', function(e) {
      if (!e.target.classList.contains('aftersell-variant-select')) return;
      var select  = e.target;
      var card    = select.closest('.aftersell-card');
      var opt     = select.options[select.selectedIndex];
      var priceEl = card.querySelector('[data-price-el]');
      var btn     = card.querySelector('.aftersell-cta');
      priceEl.innerHTML     = priceHtml(opt.getAttribute('data-price'), opt.getAttribute('data-compare'));
      btn.dataset.variantId = opt.value;
      btn.disabled          = false;
      btn.textContent       = CTA_LABEL;
      btn.className         = 'aftersell-cta';
    });

    list.addEventListener('click', function(e) {
      var btn = e.target.closest('.aftersell-cta');
      if (!btn || btn.disabled) return;
      var variantId = btn.dataset.variantId;
      if (!variantId) return;

      e.preventDefault();
      btn.disabled    = true;
      btn.textContent = 'Adding…';

      fetch('/cart/add.js', {
        method: 'POST',
        credentials: 'same-origin',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          id: parseInt(variantId, 10),
          quantity: 1,
          properties: {
            "_source": "Aftersell",
            "_attribution": "CTA"
          }
        })
      })
      .then(function(r) {
        if (!r.ok) throw new Error('Cart add failed: ' + r.status);
        return r.json();
      })
      .then(function() {
        btn.textContent = 'Added!';
        btn.classList.add('aftersell-cta--added');
        if (typeof window.theme !== 'undefined' && window.theme.cart && window.theme.cart.open) {
          window.theme.cart.open();
        }
        setTimeout(function() {
          btn.textContent = CTA_LABEL;
          btn.classList.remove('aftersell-cta--added');
          btn.disabled = false;
        }, 2500);
      })
      .catch(function(err) {
        console.error('[AfterSell carousel] Add to cart error:', err);
        btn.textContent = 'Try Again';
        btn.disabled    = false;
      });
    });
  }

  function initSplide() {
    function mount() {
      if (typeof window.Splide === 'undefined') {
        setTimeout(mount, 50);
        return;
      }

      splideInstance = new Splide('#aftersell-splide-' + SECTION_ID, {
        type:       'slide',
        perPage:    4,
        perMove:    4,
        gap:        '10px',
        pagination: false,
        arrows:     false,
        speed:      350,
        drag:       false,
        breakpoints: {
          768: { perPage: 2, perMove: 2, drag: 'free', snap: true },
          480: { perPage: 1, perMove: 1, drag: 'free', snap: true, padding: { right: '25%' } }
        }
      }).mount();

      function updateArrows() {
        var idx = splideInstance.index;
        var end = splideInstance.length - splideInstance.options.perPage;
        prevBtn.disabled = idx <= 0;
        nextBtn.disabled = idx >= end;
      }

      prevBtn.addEventListener('click', function() { splideInstance.go('<'); });
      nextBtn.addEventListener('click', function() { splideInstance.go('>'); });
      splideInstance.on('moved', updateArrows);
      updateArrows();
    }
    mount();
  }

  function clearSkeletons() { list.innerHTML = ''; }

  evaluate();
})();
</script>

{% endif %}

{% schema %}
{
  "name": "Aftersell Carousel",
  "settings": [
    { "type": "text",  "id": "strategy_id",  "label": "AfterSell Strategy ID", "default": "ADD_ID_HERE" },
    { "type": "text",  "id": "heading",      "label": "Heading",               "default": "You might also like" },
    { "type": "text",  "id": "cta_label",    "label": "CTA Button Label",      "default": "Add to cart" },
    { "type": "range", "id": "max_products", "label": "Max Products to Show",  "default": 8, "min": 1, "max": 20, "step": 1 }
  ],
  "presets": [{ "name": "Aftersell Carousel" }]
}
{% endschema %}
```

<Frame>
  <img src="https://mintcdn.com/aftersell/SnVX3h-PpMMxQMDU/images/aftersell/strategy-carousel-example.png?fit=max&auto=format&n=SnVX3h-PpMMxQMDU&q=85&s=8fb7ac08c2ae0c430b5bc4f810c59e40" alt="En Strategy-drevet produktkarrusel vist på en Shopify-produktside" width="2692" height="1146" data-path="images/aftersell/strategy-carousel-example.png" />
</Frame>

<div id="customizing">
  ### Tilpasning
</div>

Sektionsskemaet eksponerer fire indstillinger, forhandleren kan redigere: **Strategy ID**, **Heading**, **CTA Button Label** og **Max Products to Show**. Tilføj eller fjern indstillinger i `{% schema %}`-blokken for at eksponere flere justeringsmuligheder i temaeditoren.

CSS'en er afgrænset under `.aftersell-*`-klassenavne og indeholder en Splide-drevet karrusel med 4 produkter ad gangen, der skifter til 2 ved 768px og 1 ved 480px. Redigér den frit, så den matcher dit tema - intet af den er nødvendig for, at API-kaldet fungerer.

***

<div id="cart-context-custom-cart-upsell-block">
  ## Kurvkontekst: Brugerdefineret kurv-upsell-blok
</div>

Dette mønster er strukturelt det samme som PDP-mønsteret, med én afgørende forskel: **produktkontekst-arrayet** bygges fra kurvens varelinjer i stedet for det aktuelt viste produkt. Strategyen modtager derefter hver vare, kunden har tilføjet, og returnerer anbefalinger baseret på kurven som helhed.

Implementeringen bor der, hvor din brugerdefinerede kurvkode bor - en Liquid-sektion, der renderer cart draweren, en brugerdefineret blok i en headless butiksfacade eller en temaskabelon som `cart.liquid`. Formen på API-kaldet og håndteringen af svaret er identisk med PDP-eksemplet - kun `products`-arrayet er anderledes.

Strukturen ser sådan ud:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
var products = {{ cart.items | json }}.map(function(item) {
  return {
    productId: 'gid://shopify/Product/' + item.product_id,
    variantId: 'gid://shopify/ProductVariant/' + item.variant_id,
    quantity:  item.quantity,
    price:     item.price / 100
    // ...other fields as needed
  };
});
```

Resten af payloaden (cart, customer, session, cartToken) og `fetch`-kaldet til `/api/public/strategy/evaluate` er uændret fra PDP-mønsteret ovenfor - kun `products`-arrayet skifter fra `[productContext]` til det kurvafledte array.

***

<div id="what-happens-when-the-strategy-returns">
  ## Hvad der sker, når Strategyen returnerer
</div>

Svarets form er den samme, uanset hvilken kontekst du sendte:

```json theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
{
  "success": true,
  "products": [ /* enriched recommended products */ ],
  "evaluationId": "f3a1c2e0-...",
  "resolution": { "strategyId": "...", "matchedRuleIds": [...], "fallbackUsed": false },
  "meta": { "servedFromCache": false, "processingTimeMs": 12, "data": {} }
}
```

`evaluationId` er et unikt id for denne evaluering. Hvis du gemmer det og knytter det til de produkter, du viser, kan du attribuere den resulterende ordre tilbage til præcis den anbefaling, der skabte den - se [Attribution](#attribution) nedenfor.

Hvordan du viser `products`-arrayet, er helt op til din temakode. PDP-snippet ovenfor viser dem som en karrusel af kort med variantvælgere og læg-i-kurv-knapper; en brugerdefineret kurvblok kunne vise dem som en lodret liste inde i draweren.

Se [API-referencen for Evaluate Strategy](/da/aftersell/strategies_api_reference_evaluate_strategy) for det fulde anmodnings- og svarskema.

***

<div id="when-no-product-is-returned">
  ## Når der ikke returneres noget produkt
</div>

Hvis Strategyen ikke returnerer nogen produkter (`products: []`), er det op til din kode, hvordan det håndteres. PDP-snippet ovenfor skjuler karrusellen helt. En brugerdefineret kurvblok kan falde tilbage til kurvens standard-upsell-liste eller simpelthen ikke vise noget.

For at undgå et tomt svar skal du konfigurere en **Catch all** i Strategyen, så der altid er et fallback-produkt at returnere. Se siden [Opbygning af Strategies](/da/aftersell/strategies_building_in_app) for at lære, hvordan du opsætter en Catch all.

***

<div id="tips-for-custom-integrations">
  ## Tips til brugerdefinerede integrationer
</div>

* **Byg konteksten i Liquid.** Liquid kører ved rendering og har adgang til hele Shopify-objektgrafen - produkt, kurv, kunde, butik, request. Brug den til at udfylde payloaden på serversiden i stedet for at ty til klientsidekald.
* **Hold API-nøglen ude af offentlige repos.** Den ender i din temakode, som sendes til browseren - det er fint. Men indsæt ikke det samme tema i et offentligt repository, og del ikke bundtet eksternt.
* **Brug en Catch all.** Butiksfacade-oplevelser ser ødelagte ud, når en plads forsvinder. En Catch all med et lille sæt sikre standardprodukter holder brugerfladen konsistent.
* **Cache, hvor det giver mening.** Strategies API laver let caching på serversiden (`meta.servedFromCache`), men til PDP'er med høj trafik kan du også ville debounce eller memoize kald på klienten (f.eks. ikke kalde igen, når det samme produkt vises to gange i en session).

***

<div id="attribution">
  ## Attribution
</div>

Når en kunde klikker på læg-i-kurv-knappen i snippet, knytter `/cart/add.js`-kaldet **varelinjeegenskaber** til kurvvaren:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
properties: {
  "_source": "Aftersell",
  "_attribution": "CTA"
}
```

Disse egenskaber følger med varelinjen hele vejen til Shopify-ordren, hvor de vises på varelinjeposten. Du kan bruge dem senere til at attribuere omsætning, filtrere ordrer eller fodre analyseværktøjer, der læser varelinjeegenskaber.

Nøglerne og værdierne er konventioner, ikke krav - API-kaldet fungerer på samme måde, uanset hvad du angiver her. Ændr dem, så de passer til din egen attributionsmodel. For eksempel:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
properties: {
  "_source": "PDP Carousel",
  "_strategy_id": "{{ section.settings.strategy_id }}",
  "_campaign": "summer-2026"
}
```

<Tip>
  Egenskabsnøgler, der begynder med en understregning (`_`), er skjult i kurv- og checkout-brugerfladen, men knyttes stadig til ordren. Brug understregningspræfikset til attributionsmetadata, du ikke ønsker, at kunderne skal se.
</Tip>

Anvend samme mønster i kurvkontekst-implementeringen - ethvert læg-i-kurv-kald, du foretager fra en brugerdefineret upsell-blok, kan bære de egenskaber, du har brug for.

<div id="attributing-back-to-the-evaluation">
  ### Attribuering tilbage til evalueringen
</div>

For at knytte en ordre tilbage til den **præcise evaluering**, der anbefalede produktet - i stedet for blot "kom fra en Strategy" - skal du gemme `evaluationId` fra svaret og knytte det til varelinjen under egenskaben `__as_offer_id`. AfterSell læser denne nøgle, så ordrer mærket med den attribueres til den specifikke evaluering i rapporteringen.

I `evaluate()`-handleren skal du holde fast i id'et fra svaret:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
.then(function(data) {
  if (!data || !data.success || !data.products || !data.products.length) {
    clearSkeletons(); return;
  }
  evaluationId = data.evaluationId; // declare `var evaluationId;` alongside your other state
  renderCards(data.products.slice(0, MAX_PRODUCTS));
})
```

Inkludér det derefter i læg-i-kurv-egenskaberne:

```js theme={"theme":{"light":"snazzy-light","dark":"github-dark"}}
properties: Object.assign(
  { "_source": "Aftersell", "_attribution": "CTA" },
  evaluationId ? { "__as_offer_id": evaluationId } : {}
)
```

Behold den dobbelte understregning i `__as_offer_id` - det er den nøgle, AfterSell leder efter, og understregningspræfikset holder den skjult for kunderne. Hvis `evaluationId` mangler (for eksempel hvis der ikke blev returneret produkter), skal du springe egenskaben over i stedet for at sende en tom værdi.
