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 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.
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 –af_click,af_t,af_storeogaf_srv– i svaret fra jeres vært.af_srv=1fortæ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.
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.
Sæt Workeren op
- Kopier kodestumpen fra Program → Opsætning – på script-trinnet under Bag Cloudflare? Gem klikket fra jeres egen vært. Her står
DIN_BUTIKi stedet for jeres butiks-id. - Gå til Workers & Pages i Cloudflare, og opret en ny Worker. Tryk Edit code, erstat koden med kodestumpen, og tryk Deploy.
- Gå til Workerens Settings → Domains & Routes, og tilføj ruterne
shop.dk/*ogwww.shop.dk/*i jeres zone. - Tjek under DNS, at posterne for
shop.dkogwwwhar proxy slået til (orange sky). Ellers kører Workeren ikke. - Lander kunderne på et andet værtsnavn end det, de betaler på – fx
www.shop.dkogcheckout.shop.dk– så sætAF_COOKIE_DOMAINtil jeres domæne, fx'.shop.dk', og giv scriptet samme domæne meddata-cookie-domain.
// 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.
Test
- 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.
- Åbn browserens udviklerværktøjer. Under Netværk skal svaret for siden have
Set-Cookie: af_click=…medMax-Age, og under Lager → Cookies skalaf_click,af_t,af_storeogaf_srvstå på jeres vært. - Læg en testordre. Live-testen i Opsætning viser klikket og købet som før.
Mangler du noget?
Skriv til os – vi svarer hurtigt, og siden bliver rettet.