# Cloudflare Worker

> Store the affiliate click as a cookie from your own host with a Cloudflare Worker – for platforms where you can't put code on the server.

Safari shortens cookies and storage written by JavaScript to about 7 days without a visit. Safari treats a cookie that your own server sets in the page's response differently – that's why [server-side tracking](https://www.affilyflow.com/en/docs/store/server-side-tracking) exists. But on Smartweb, DanDomain, Shoporama, Magento, PrestaShop and Shopware you can't put our server code. If your domain has its DNS at Cloudflare, a Cloudflare Worker can do the same: it sets the click cookie in the response from your own host.

> [!WARNING]
> We are verifying the effect in Safari with a test over 8 days and promise nothing about the cookie's lifetime until it is done. The Worker does no harm in the meantime: the script and the sale work exactly as before, and the same order only counts once.

The Worker is an addition. The script must still be on every page, and the sale is reported as today – by the platform, the script or your server. For Shopify you use the app, for WooCommerce the plugin. If your shop is hosted by its platform, check with the platform first that it allows Cloudflare's proxy in front – Webflow advises against it, for example.

## How it works

- When a customer lands with `?afid=` from an affiliate link, the Worker sets the same four cookies as the [server code](https://www.affilyflow.com/en/docs/store/server-side-tracking#the-click-from-your-server) – `af_click`, `af_t`, `af_store` and `af_srv` – in the response from your host. `af_srv=1` tells the script not to write the cookie again.
- On a return visit the cookies are renewed with the time that's left – never beyond 90 days from the click. The program's own cookie period is still enforced on our server.
- Only pages (HTML) fetched with `GET`. Images, scripts, forms and API calls are left alone.
- A new click deletes an older copy the script wrote on the whole domain, so the customer never carries two different clicks.
- If something goes wrong in the Worker, the customer still gets the page. Tracking must never break your site.

> [!NOTE]
> The Worker must run on your own host – `shop.dk` and `www.shop.dk` – never on a subdomain such as `track.shop.dk` that points to us. Safari treats a host that points on to a third party as a third party.

## Consent

The Worker follows your choice under **Cookie banner** in Setup. The snippet in Setup has the choice filled in as `AF_WAIT_FOR_CONSENT`.

- **Wait for consent** (`true`): the Worker reads the customer's choice in the banner's cookie – Cookiebot, Cookie Information and Smartweb's own banner. Yes to marketing: the click is stored and renewed. An active no: nothing is stored, and a click that was already stored is deleted. No answer yet: nothing is stored.
- **Not waiting for consent** (`false`): the click is stored on landing, as the script does. An active no in one of the banners above still deletes it.

When a customer lands from an affiliate link, the banner hasn't been answered yet. The script then stores the click on a yes, and on the next page the Worker takes over the cookie and renews it from your host. If you use another banner, the function `affilyflowConsent` must be adapted to yours – otherwise the Worker stores nothing while you wait for consent. See [Consent and cookies](https://www.affilyflow.com/en/docs/store/consent-and-cookies).

## Set up the Worker

1. Copy the snippet from **Program → Setup** – on the script step under **Behind Cloudflare? Store the click from your own host**. Here, `DIN_BUTIK` stands in for your store ID.
2. Go to **Workers & Pages** in Cloudflare and create a new Worker. Press **Edit code**, replace the code with the snippet and press **Deploy**.
3. Go to the Worker's **Settings → Domains & Routes** and add the routes `shop.dk/*` and `www.shop.dk/*` in your zone.
4. Check under **DNS** that the records for `shop.dk` and `www` have the proxy switched on (orange cloud). Otherwise the Worker doesn't run.
5. If customers land on a different host name from the one they pay on – for example `www.shop.dk` and `checkout.shop.dk` – set `AF_COOKIE_DOMAIN` to your domain, for example `'.shop.dk'`, and give the script the same domain with `data-cookie-domain`.

```js title="Cloudflare Worker · with Wait for consent" group="cloudflare-worker"
// Cloudflare Worker – Affilyflow: gem affiliate-klikket som en cookie fra jeres EGEN vært (i sidens
// svar) i stedet for kun fra JavaScript, som Safari forkorter til cirka 7 dage. Effekten i Safari er
// ved at blive efterprøvet – se affilyflow.com/docs/butik/cloudflare.
// Kræver DNS hos Cloudflare med proxy slået til (orange sky). Læg Workeren på jeres egen vært under
// Workers Routes, fx shop.dk/* og www.shop.dk/* – aldrig på et subdomæne, der peger på os.
// Scriptet skal stadig ligge på alle sider, og salget meldes som før. Cookierne er ikke HttpOnly
// (track.js læser dem).
const AF_STORE = 'DIN_BUTIK'; // jeres butiks-id
// Vent på samtykke – samme valg som "Cookiebanner" i Opsætning. true: klikket gemmes først, når
// kunden har sagt ja til marketing; scriptet gemmer det ved et ja, og Workeren overtager cookien
// på næste side. Et aktivt nej sletter klikket.
const AF_WAIT_FOR_CONSENT = true;
// Lander kunden på www.shop.dk og betaler på shop.dk eller checkout.shop.dk? Så sæt domænet her –
// og samme domæne i scriptets data-cookie-domain, hvis det også ligger på siderne.
const AF_COOKIE_DOMAIN = null; // fx '.shop.dk'
// Valgfrit: true sender /af/* videre til Affilyflow, så scriptet kan hentes fra jeres eget domæne:
//   <script src="/af/t/DIN_BUTIK.js" async></script>
const AF_PROXY = false;
const AF_PROXY_KEY = null; // proxy-nøglen fra Opsætning (valgfri)

const AF_ID = /^c_[A-Za-z0-9_-]{4,64}$/;
const AF_MAX = 90 * 86400;
const AF_KEYS = ['af_click', 'af_t', 'af_store', 'af_srv'];

export default {
  async fetch(request) {
    const url = new URL(request.url);
    if (AF_PROXY && (url.pathname === '/af' || url.pathname.startsWith('/af/'))) return affilyflowProxy(request, url);
    const response = await fetch(request);
    // Kun sider (HTML), der hentes med GET – billeder, scripts og formularer røres ikke.
    if (request.method !== 'GET' || !(response.headers.get('Content-Type') || '').includes('text/html')) return response;
    let lines = [];
    try {
      lines = affilyflowClick(request, url);
    } catch (e) {
      console.warn('Affilyflow:', e); // sporingen må aldrig vælte siden
    }
    if (!lines.length) return response;
    const out = new Response(response.body, response);
    for (const line of lines) out.headers.append('Set-Cookie', line);
    return out;
  },
};

// Set-Cookie-linjerne til svaret.
function affilyflowClick(request, url) {
  const out = [];
  const jar = affilyflowJar(request.headers.get('Cookie'));
  const secure = url.protocol === 'https:';
  const line = (name, value, age, domain) =>
    name + '=' + value + '; Max-Age=' + age + '; Path=/; SameSite=Lax' + (secure ? '; Secure' : '') + (domain ? '; Domain=' + domain : '');
  // En ældre kopi, scriptet har skrevet på hele domænet, må ikke skygge for jeres cookie.
  const wipe = (keys) => {
    if (!AF_COOKIE_DOMAIN) for (const d of affilyflowDomains(url.hostname)) for (const k of keys) out.push(line(k, '', 0, d));
  };
  const consent = affilyflowConsent(jar);
  if (consent === false) {
    // Aktivt nej til marketing: slet klikket.
    const set = AF_KEYS.filter((k) => k in jar);
    for (const k of set) out.push(line(k, '', 0, AF_COOKIE_DOMAIN));
    wipe(set);
    return out;
  }
  if (consent === null && AF_WAIT_FOR_CONSENT) return out; // intet svar endnu – scriptet gemmer klikket ved et ja
  const save = (click, t, age) => {
    for (const [k, v] of [['af_click', click], ['af_t', String(t)], ['af_store', AF_STORE], ['af_srv', '1']]) out.push(line(k, v, age, AF_COOKIE_DOMAIN));
  };
  const now = Math.floor(Date.now() / 1000);
  const afid = url.searchParams.get('afid') || '';
  if (AF_ID.test(afid)) {
    // Nyt klik fra et affiliate-link.
    wipe(['af_click', 'af_t']);
    save(afid, now, AF_MAX);
    return out;
  }
  // Genbesøg: forny cookien – men aldrig ud over 90 dage fra klikket.
  const click = jar.af_click || '';
  const t = Number(jar.af_t || 0);
  const left = AF_MAX - (now - t);
  if (t > 0 && left > 0 && AF_ID.test(click)) save(click, t, left);
  return out;
}

// Samtykke til marketing: true = ja, false = aktivt nej, null = intet svar endnu.
function affilyflowConsent(cookies) {
  try {
    // Cookiebot (-1 = kunden er uden for et område, hvor der kræves samtykke)
    const cb = decodeURIComponent(cookies.CookieConsent || '');
    if (cb === '-1' || cb.includes('marketing:true')) return true;
    if (cb.includes('marketing:false')) return false;
    // Cookie Information
    const ci = JSON.parse(decodeURIComponent(cookies.CookieInformationConsent || 'null'));
    if (ci && typeof ci === 'object') return Array.isArray(ci.consents_approved) && ci.consents_approved.includes('cookie_cat_marketing');
    // Smartwebs eget banner: de valgte kategorier, fx REQUIRED,STATISTICS,MARKETING
    const sw = decodeURIComponent(cookies.cookie_consent || '');
    if (sw) return /(^|,)\s*MARKETING\s*(,|$)/i.test(sw);
  } catch {
    // en cookie, der ikke kan læses, tæller som intet svar
  }
  return null;
}

// Cookie-headeren som { navn: værdi } – den første værdi vinder, som når browseren læser den.
function affilyflowJar(header) {
  const jar = Object.create(null);
  for (const part of (header || '').split(';')) {
    const i = part.indexOf('=');
    if (i < 1) continue;
    const name = part.slice(0, i).trim();
    if (!(name in jar)) jar[name] = part.slice(i + 1).trim();
  }
  return jar;
}

// Værten og domænerne over den (www.shop.dk → www.shop.dk og shop.dk).
function affilyflowDomains(host) {
  if (/^[\d.]+$/.test(host) || host.includes(':')) return [];
  const parts = host.split('.');
  const out = [];
  for (let i = 0; i <= parts.length - 2; i++) out.push(parts.slice(i).join('.'));
  return out;
}

// /af/* → Affilyflow. Jeres kunders cookies sendes aldrig med.
async function affilyflowProxy(request, url) {
  const headers = new Headers(request.headers);
  for (const h of ['Cookie', 'Host', 'X-Affilyflow-Proxy', 'X-Affilyflow-Client-IP']) headers.delete(h);
  const ip = request.headers.get('CF-Connecting-IP');
  if (AF_PROXY_KEY && ip) {
    // Kundens IP-adresse til vores værn mod misbrug – bruges kun som en nøgle, der skifter hver dag.
    headers.set('X-Affilyflow-Proxy', AF_PROXY_KEY);
    headers.set('X-Affilyflow-Client-IP', ip);
  }
  const body = request.method === 'GET' || request.method === 'HEAD' ? undefined : request.body;
  return fetch('https://app.affilyflow.com' + url.pathname.slice(3) + url.search, { method: request.method, headers, body, redirect: 'manual' });
}
```

The Worker runs on every request on the routes – images too – and passes them on unchanged. Cloudflare's free plan has a daily limit on the number of Worker requests. If your shop has a lot of traffic, check your plan with Cloudflare.

## Your own proxy (optional)

If you set `AF_PROXY = true`, the Worker also passes everything under `/af/` on to us, so the script and its requests go through your own domain, and an ad blocker only sees your own addresses. Then replace the script in `<head>` with `<script src="/af/t/DIN_BUTIK.js" async></script>`. Your customers' cookies aren't sent along. If the snippet in Setup shows a proxy key, the Worker also sends the customer's IP address, so our abuse protection counts per customer – see [Your own proxy](https://www.affilyflow.com/en/docs/store/server-side-tracking#your-own-proxy-optional).

## Test

1. Go to **Test tracking** in Setup and press **Open test link**. If you wait for consent, say yes to marketing in the banner and open one more page.
2. Open the browser's developer tools. Under **Network**, the response for the page should have `Set-Cookie: af_click=…` with `Max-Age`, and under **Storage → Cookies**, `af_click`, `af_t`, `af_store` and `af_srv` should be on your host.
3. Place a test order. The live test in Setup shows the click and the purchase as before.

Source: https://www.affilyflow.com/en/docs/store/cloudflare
