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.

View as Markdown

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 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.

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 – 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.

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.

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.
Cloudflare Worker · with Wait for consentJavaScript
// 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.

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.

Missing something?

Write to us – we answer quickly, and the page gets fixed.

Contact support