# Cloudflare Worker

> Gem affiliate-klikket som en cookie fra jeres egen vært med en Cloudflare Worker – til platforme, hvor I ikke kan lægge kode på serveren.

Safari forkorter cookies og lager, som JavaScript skriver, til cirka 7 dage uden besøg. En cookie, som jeres egen server sætter i sidens svar, behandler Safari anderledes – det er derfor, [sporing fra jeres server](https://www.affilyflow.com/docs/butik/server-sporing) findes. Men på Smartweb, DanDomain, Shoporama, Magento, PrestaShop og Shopware kan I ikke lægge vores serverkode. Har jeres domæne DNS hos Cloudflare, kan en Cloudflare Worker gøre det samme: den sætter klik-cookien i svaret fra jeres egen vært.

> [!WARNING]
> Vi efterprøver effekten i Safari med en test over 8 dage og lover intet om cookiens levetid, før den er færdig. Workeren gør ingen skade i mellemtiden: scriptet og salget virker præcis som før, og samme ordre tæller kun en gang.

Workeren er en tilføjelse. Scriptet skal stadig ligge på alle sider, og salget meldes som i dag – af platformen, scriptet eller jeres server. Til Shopify bruger I appen, til WooCommerce pluginnet. Ligger shoppen hos en platform, der står for hostingen, så tjek først med platformen, at den tillader Cloudflares proxy foran – Webflow fraråder det fx.

## Sådan virker den

- Lander en kunde med `?afid=` fra et affiliate-link, sætter Workeren de samme fire cookies som [serverkoden](https://www.affilyflow.com/docs/butik/server-sporing#klikket-fra-jeres-server) – `af_click`, `af_t`, `af_store` og `af_srv` – i svaret fra jeres vært. `af_srv=1` fortæller scriptet, at det ikke skal skrive cookien igen.
- Ved et genbesøg fornyes cookierne med den resterende tid – aldrig ud over 90 dage fra klikket. Programmets egen cookie-periode håndhæves stadig på vores server.
- Kun sider (HTML), der hentes med `GET`. Billeder, scripts, formularer og API-kald røres ikke.
- Et nyt klik sletter en ældre kopi, scriptet har skrevet på hele domænet, så kunden aldrig bærer to forskellige klik.
- Går noget galt i Workeren, får kunden siden alligevel. Sporingen må aldrig vælte jeres side.

> [!NOTE]
> Workeren skal ligge på jeres egen vært – `shop.dk` og `www.shop.dk` – aldrig på et subdomæne som `track.shop.dk`, der peger på os. Safari behandler en vært, der peger videre til en tredjepart, som tredjepart.

## Samtykke

Workeren følger jeres valg under **Cookiebanner** i Opsætning. Kodestumpen i Opsætning har valget sat ind i `AF_WAIT_FOR_CONSENT`.

- **Vent på samtykke** (`true`): Workeren læser kundens valg i bannerets cookie – Cookiebot, Cookie Information og Smartwebs eget banner. Ja til marketing: klikket gemmes og fornyes. Aktivt nej: intet gemmes, og et klik, der allerede er gemt, slettes. Intet svar endnu: intet gemmes.
- **Venter ikke på samtykke** (`false`): klikket gemmes ved landing, som scriptet gør. Et aktivt nej i et af bannerne ovenfor sletter det stadig.

Når kunden lander fra et affiliate-link, har banneret endnu ikke fået et svar. Så gemmer scriptet klikket ved et ja, og på næste side overtager Workeren cookien og fornyer den fra jeres vært. Bruger I et andet banner, skal funktionen `affilyflowConsent` rettes til jeres – ellers gemmer Workeren intet, når I venter på samtykke. Se [Samtykke og cookies](https://www.affilyflow.com/docs/butik/samtykke-og-cookies).

## Sæt Workeren op

1. Kopier kodestumpen fra **Program → Opsætning** – på script-trinnet under **Bag Cloudflare? Gem klikket fra jeres egen vært**. Her står `DIN_BUTIK` i stedet for jeres butiks-id.
2. Gå til **Workers & Pages** i Cloudflare, og opret en ny Worker. Tryk **Edit code**, erstat koden med kodestumpen, og tryk **Deploy**.
3. Gå til Workerens **Settings → Domains & Routes**, og tilføj ruterne `shop.dk/*` og `www.shop.dk/*` i jeres zone.
4. Tjek under **DNS**, at posterne for `shop.dk` og `www` har proxy slået til (orange sky). Ellers kører Workeren ikke.
5. Lander kunderne på et andet værtsnavn end det, de betaler på – fx `www.shop.dk` og `checkout.shop.dk` – så sæt `AF_COOKIE_DOMAIN` til jeres domæne, fx `'.shop.dk'`, og giv scriptet samme domæne med `data-cookie-domain`.

```js title="Cloudflare Worker · med Vent på samtykke" 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' });
}
```

Workeren kører på hver anmodning på ruterne – også billeder – og sender dem videre uændret. Cloudflares gratis plan har et dagligt loft over antal anmodninger til Workers. Har jeres shop meget trafik, så tjek jeres plan hos Cloudflare.

## Egen proxy (valgfri)

Sætter I `AF_PROXY = true`, sender Workeren også alt under `/af/` videre til os, så scriptet og dets kald går gennem jeres eget domæne, og en annonceblokker kun ser jeres egne adresser. Skift så scriptet i `<head>` ud med `<script src="/af/t/DIN_BUTIK.js" async></script>`. Jeres kunders cookies sendes ikke med. Står der en proxy-nøgle i kodestumpen i Opsætning, sender Workeren også kundens IP-adresse med, så vores værn mod misbrug tæller pr. kunde – se [Egen proxy](https://www.affilyflow.com/docs/butik/server-sporing#egen-proxy-valgfri).

## Test

1. Gå til **Test sporingen** i Opsætning, og tryk **Åbn testlink**. Venter I på samtykke, så sig ja til marketing i banneret, og åbn en side mere.
2. Åbn browserens udviklerværktøjer. Under **Netværk** skal svaret for siden have `Set-Cookie: af_click=…` med `Max-Age`, og under **Lager → Cookies** skal `af_click`, `af_t`, `af_store` og `af_srv` stå på jeres vært.
3. Læg en testordre. Live-testen i Opsætning viser klikket og købet som før.

Kilde: https://www.affilyflow.com/docs/butik/cloudflare
