<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="da-DK"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://www.jacobworsoe.dk/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.jacobworsoe.dk/" rel="alternate" type="text/html" hreflang="da-DK" /><updated>2026-03-25T11:25:47+00:00</updated><id>https://www.jacobworsoe.dk/feed.xml</id><title type="html">jacobworsoe.dk</title><subtitle>Analytics ninja og datanørd</subtitle><entry><title type="html">Kan browser fingerprinting erstatte cookies?</title><link href="https://www.jacobworsoe.dk/browser-fingerprinting-erstatte-cookies/" rel="alternate" type="text/html" title="Kan browser fingerprinting erstatte cookies?" /><published>2022-01-11T22:01:18+00:00</published><updated>2022-01-11T22:01:18+00:00</updated><id>https://www.jacobworsoe.dk/browser-fingerprinting-erstatte-cookies</id><content type="html" xml:base="https://www.jacobworsoe.dk/browser-fingerprinting-erstatte-cookies/"><![CDATA[<p>Tilbage i 2019 satte jeg mig for at undersøge om browser fingerprinting kunne være en erstatning for cookies til web analytics. Jeg indsamlede en masse data og <a href="https://www.linkedin.com/posts/laust-kehlet-741158a_5-danske-5-internationale-digitale-marketingblogs-activity-6885129039544160256-XCXn/">for nyligt blev jeg motiveret</a> til at få det analyseret og udgive et nyt blogindlæg.</p>

<figure><a href="/assets/images/2022/01/Fingerprint-vs-sessions.jpg"><img src="/assets/images/2022/01/Fingerprint-vs-sessions-860x213.jpg" alt="Fingerprint data blev indsamlet tilbage i 2019-2020." width="860" height="213" class="size-large wp-image-2746" /></a><figcaption>Fingerprint data blev indsamlet tilbage i 2019-2020.</figcaption></figure>

<h2>Cookies er geniale men begrænsede</h2>

<p>De seneste år er cookies blevet stærkt begrænset som redskab til at tracke brugere. Både fordi 3. part cookies er blevet begrænset med fx <a href="/itp-og-google-analytics/">ITP</a> men også fordi brugere nu skal give consent til cookies og dermed tracking.</p>

<p>Cookies er geniale til at genkende en bestemt browser på tværs af sidevisninger og sessioner. Det er helt anonymt og brugeren har fuld kontrol og kan slette cookies efter behov.</p>

<h2>Browser fingerprinting</h2>

<p>Et alternativ til at sætte en cookie med et anonymt ID kan være at forsøge at genkende og adskille browsere fra hinanden, baseret på alle de indstillinger og informationer man kan se om en given bruger med JavaScript. Dette kaldes <a href="https://en.wikipedia.org/wiki/Device_fingerprint">browser fingerprinting</a>.</p>

<p>Browser fingerprinting er stærkt kritiseret, fordi det sker uden at brugeren er klar over det og <a href="https://blog.mozilla.org/security/2020/01/07/firefox-72-fingerprinting/">browsere forsøger derfor at blokere for det</a>.</p>

<p>På <a href="https://amiunique.org/">amiunique.org</a> kan du se om der kan laves et unikt fingerprint af din browsere og se hvilke datapunkter der indgår i et fingerprint.</p>

<h2>Hvad indgår i et fingerprint?</h2>

<p>Her er en ikke-udtømmende liste over hvad der kan indgå i et fingerprint.</p>

<ul>
<li>User Agent</li>
<li>Sprog</li>
<li>Operativ system</li>
<li>Tidszone</li>
<li>Hvilke fonts der er installeret</li>
<li>Om der bruges Adblocker</li>
<li>Browser</li>
<li>Browser version</li>
<li>Om Java er aktiveret</li>
<li>Hvilke plugins der er installeret</li>
<li>Skærm opløsning</li>
<li>Om browseren må bruge kamera og mikrofon</li>
<li>Og mange flere...</li>
</ul>

<p>Men også ting som batteri status og om ens statuslinje er vist eller skjult.</p>

<p><a href="/assets/images/2021/08/Fingerprint-data-points.jpg"><img src="/assets/images/2021/08/Fingerprint-data-points-860x452.jpg" alt="" width="860" height="452" class="aligncenter size-large wp-image-2709" /></a></p>

<p>Se hele listen på <a href="https://amiunique.org/">amiunique.org</a>.</p>

<p>Men spørgsmålet er om det ovenhovedet gør browseren unik?</p>

<h2>Kan fingerprinting erstatte cookies?</h2>

<p>Fra et teknisk synspunkt er det interessant at teste om fingerprinting overhovedet kan være en erstatning for cookies i forhold til at identificere brugere i Google Analytics. Jeg lavede en test hvor jeg brugte <a href="https://github.com/fingerprintjs/fingerprintjs">fingerprintjs</a> til at generere et fingerprint og gemme det com custom dimension i Google Analytics. Selvfølgelig <a href="http://joshwayman.com/how-to-implement-fingerprint-js-with-gtm/">opsat i GTM</a>.</p>

<p><a href="https://github.com/fingerprintjs/fingerprintjs">Fingerprintjs</a> laver et Array med alle datapunkterne som derefter hashes så man får en værdi som fx <code>69a45868bbc98e83a463e2e0730be988</code>.</p>

<figure><a href="/assets/images/2021/08/Fingerprint-components-console.png"><img src="/assets/images/2021/08/Fingerprint-components-console-860x495.png" alt="Datapunkter der indgår i et fingerprintjs fingerprint som derefter hashes." width="860" height="495" class="size-large wp-image-2718" /></a><figcaption>Datapunkter som <a href="https://github.com/fingerprintjs/fingerprintjs">fingerprintjs</a> bruger til at lave et fingerprint som derefter hashes.</figcaption></figure>

<p>Herunder ses nogle eksempler på fingerprints, device, browser samt hvor mange GA users der har præcis denne kombination. Hvis et fingerprint skal være unikt, skal det kun matche én bestemt User. Ellers er der dermed flere browsere som har det samme fingerprint og så kan det ikke bruges til at genkende én browser mellem sidevisninger og besøg, som en cookie kan.</p>

<p><a href="/assets/images/2021/08/Fingerprint-10-random-fingerprints.jpg"><img src="/assets/images/2021/08/Fingerprint-10-random-fingerprints-860x352.jpg" alt="" width="860" height="352" class="aligncenter size-large wp-image-2712" /></a></p>

<p>Og her kan man allerede se problemet, hvor flere Users har det samme fingerprint.</p>

<p>Hvis vi kigger på hele datasættet opdelt i desktop og mobile er der dog en stor andel af fingerprint værdierne der kun har én GA user, så de er unikke.</p>

<p><a href="/assets/images/2021/08/Fingerprint-Record-count.jpg"><img src="/assets/images/2021/08/Fingerprint-Record-count-860x430.jpg" alt="" width="860" height="430" class="aligncenter size-large wp-image-2713" /></a></p>

<p>Langt størstedelen af desktop har kun én User, men der er stadig 26 fingerprints som er fælles for mere end 5 GA Users. På mobile ser det dog anderledes ud, hvor der stadig er flest som matcher én User, men der er mange som matcher mere end én.</p>

<p>Dette skyldes at fingerprint afhænger af hvor meget man har ændret indstillingerne på sin computer og browser, samt installeret fonts og lignende. Og det gør man bare ikke i nær så høj grad på mobile.</p>

<p>Derudover er der også mange flere forskellige computere, med forskellige skærmopløsninger, osv. 
På mobile i Danmark er der rigtig mange brugere fordelt på de nyeste modeller af iPhone.</p>

<p>Men det bliver værre endnu.</p>

<p>For som det ses herover er der 200 fingerprints som matcher mere end 5 users og der er faktisk ét fingerprint som er ens for 128 forskellige browsere. Der er altså 128 iPhones som er 100% identiske i forhold til alle de datapunkter som fingerprintet bruger.</p>

<p><a href="/assets/images/2021/08/Fingerprint-Top-10-fingerprints.jpg"><img src="/assets/images/2021/08/Fingerprint-Top-10-fingerprints-860x283.jpg" alt="" width="860" height="283" class="aligncenter size-large wp-image-2711" /></a></p>

<p>Herunder ses antal users i datasættet fordelt på Device Category og Operating System. Der er 6304 users på iPhone og iPad, så når der er 128 brugere med samme fingerprint, så er det 2% af alle iPhones/iPads der har identisk fingerprint. Ikke særlig unikt, må man sige.</p>

<p><a href="/assets/images/2022/01/Users-by-device-and-os.jpg"><img src="/assets/images/2022/01/Users-by-device-and-os-860x326.jpg" alt="" width="860" height="326" class="aligncenter size-large wp-image-2742" /></a></p>

<p>For at se den totale udfordring med at bruge fingerprint til at gøre browsere unikke, har jeg vist summen af GA Users i de enkelte buckets herunder.</p>

<p><a href="/assets/images/2021/08/Fingerprint-User-count-buckets.jpg"><img src="/assets/images/2021/08/Fingerprint-User-count-buckets-860x431.jpg" alt="" width="860" height="431" class="aligncenter size-large wp-image-2714" /></a></p>

<p>Her ses det at der fx for mobile er 3655 users som har et unikt fingerprint. Men der findes samtidig 3923 users som har et fingerprint som deles af mere end 5 brugere.</p>

<p>Hvis vi opdeler dem i to grupper: Unikke fingerprints (som kun matcher én GA user) og ikke unikke fingerprints, så ser fordelingen således ud.</p>

<p><a href="/assets/images/2021/08/Fingerprint-Unikt-vs-ikke-unikt-vs-device.jpg"><img src="/assets/images/2021/08/Fingerprint-Unikt-vs-ikke-unikt-vs-device-860x428.jpg" alt="" width="860" height="428" class="aligncenter size-large wp-image-2733" /></a></p>

<p>På mobile er det altså kun 37% af brugere der har et unikt fingerprint, mens det på desktop er 89%. Totalt set har 58% af brugerne et unikt fingerprint og når andelen af trafik fra mobile tilmed er stigende er det derfor totalt umuligt at bruge fingerprint til at identificere og genkende brugere og dermed være en erstatning for cookies.</p>

<h2>Andre alternativer til cookies</h2>

<p><a href="https://plausible.io/">Plausible Analytics</a> er en ny analytics platform, som kan lave (simpel) analytics uden brug af cookies.</p>

<p><a href="/assets/images/2022/01/plausible-analytics-jacobworsoe-dk.jpg"><img src="/assets/images/2022/01/plausible-analytics-jacobworsoe-dk-860x777.jpg" alt="" width="860" height="777" class="aligncenter size-large wp-image-2737" /></a></p>

<p>I stedet for cookies bruger laver de et hash af IP adresse og User Agent samt et daily salt. Derved kan de genkende den samme bruger på den samme dag, men næste dag vil der være et nyt salt og så vil brugere være en ny unik bruger. Teoretisk vil der sagtens kunne være flere forskellige brugere fra den samme IP som har samme User Agent, men i praksis vil det være marginalt, så det er et OK kompromis. Meget bedre end fingerprinting, som faktisk ikke bruger IP adressen.</p>

<p>Under afsnittet <a href="https://plausible.io/data-policy">How we count unique users without cookies</a> kan du læse mere om hvordan Plausible Analytics tæller unikke brugere uden cookies. Eller du kan læse det her:</p>

<p><a href="/assets/images/2022/01/Plausible-analytics-unique-visitors-without-cookies.jpg"><img src="/assets/images/2022/01/Plausible-analytics-unique-visitors-without-cookies.jpg" alt="" width="693" height="1232" class="aligncenter size-full wp-image-2738" /></a></p>]]></content><author><name></name></author><category term="Analytics" /><summary type="html"><![CDATA[Tilbage i 2019 satte jeg mig for at undersøge om browser fingerprinting kunne være en erstatning for cookies til web analytics. Jeg indsamlede en masse data og for nyligt blev jeg motiveret til at få det analyseret og udgive et nyt blogindlæg.]]></summary></entry><entry><title type="html">Undgå sideskift ved formular submit</title><link href="https://www.jacobworsoe.dk/undgaa-sideskift-ved-formular-submit/" rel="alternate" type="text/html" title="Undgå sideskift ved formular submit" /><published>2021-08-12T16:46:31+00:00</published><updated>2021-08-12T16:46:31+00:00</updated><id>https://www.jacobworsoe.dk/undgaa-sideskift-ved-formular-submit</id><content type="html" xml:base="https://www.jacobworsoe.dk/undgaa-sideskift-ved-formular-submit/"><![CDATA[<p>Her er et lille trick som jeg bruger når jeg skal teste tracking af en formular. Du kan affyre denne kode i konsollen og undgå at side skifter når formularen submittes. Dermed mister du ikke debug information i konsollen eller i diverse tracking debug extensions.</p>

<p>Det eneste du skal gøre er at udskifte <code>#formId</code> med ID eller class på formularen og affyre koden i konsollen, inden du submitter formularen.</p>

<pre><code class="language-javascript">window.addEventListener('beforeunload', function(e) {
    e.preventDefault();
    e.returnValue = '';
});

document.querySelector("#formId").addEventListener("submit", function(e){    
    e.preventDefault();
    console.log("form submitted");    
});
</code></pre>]]></content><author><name></name></author><category term="Analytics" /><summary type="html"><![CDATA[Her er et lille trick som jeg bruger når jeg skal teste tracking af en formular. Du kan affyre denne kode i konsollen og undgå at side skifter når formularen submittes. Dermed mister du ikke debug information i konsollen eller i diverse tracking debug extensions.]]></summary></entry><entry><title type="html">Machine Learning med Excel</title><link href="https://www.jacobworsoe.dk/machine-learning-med-excel/" rel="alternate" type="text/html" title="Machine Learning med Excel" /><published>2021-07-24T16:11:16+00:00</published><updated>2021-07-24T16:11:16+00:00</updated><id>https://www.jacobworsoe.dk/machine-learning-med-excel</id><content type="html" xml:base="https://www.jacobworsoe.dk/machine-learning-med-excel/"><![CDATA[<blockquote><p>Mennesker lærer af erfaring. Maskiner lærer af data.</p></blockquote>

<p>Interessen for Machine Learning er steget markant i løbet af de sidste år, men for mange er det stadig mest et buzz word. Her vil jeg prøve at bringe det lidt ned på et niveau som de fleste kan relatere til, nemlig ved at lave lidt Machine Learning i Excel. Machine Learning behøver ikke være mere kompleks end det.</p>

<p><a href="/assets/images/2021/07/machine-learning-google-trends.png"><img src="/assets/images/2021/07/machine-learning-google-trends-860x264.png" alt="machine learning google trends" width="860" height="264" class="aligncenter size-large wp-image-2692" /></a></p>

<h2>Antal ord og organisk trafik</h2>

<p>Som eksempel på machine learning der kan laves i Excel, vil jeg analysere forholdet mellem længden på blogindlæg og mængden af organisk SEO trafik. Sammenhængen er bevist flere gange tidligere, fx af Backlinko:</p>

<figure><a href="/assets/images/backlinko-content-total-word-count.png"><img src="/assets/images/backlinko-content-total-word-count-690x475.png" alt="Kilde: https://backlinko.com/search-engine-ranking" width="690" height="475" class="size-medium wp-image-1297" /></a><figcaption>Kilde: <a href="https://backlinko.com/search-engine-ranking">https://backlinko.com/search-engine-ranking</a></figcaption></figure>

<p>Rand Fiskin har dog et helt andet take på det i denne Whiteboard Friday:</p>

<figure><a href="https://moz.com/blog/blog-post-length-frequency?wvideo=vhkmto6gk4"><img src="/assets/images/5995f3471cf5f3.webp" alt="The perfect blog post length and frequency is bullshit" width="690" height="475" class="size-medium wp-image-1297" /></a><figcaption>The perfect blog post length and frequency is bullshit</figcaption></figure>

<p>Her vil jeg derfor analysere hvor meget længden af indlægget betyder for trafikken på min personlige blog og endnu mere spændende: Om man kan forudsige hvor meget trafik et blogindlæg vil få, baseret på antal ord = prædiktiv analyse.</p>

<h2>Først har vi brug for noget data</h2>

<p>Vi skal bruge et datasæt som indeholder antal ord i hvert blogindlæg samt antal organiske sessioner der er startet på hvert blogindlæg. Der er flere måder at få antal ord på. Man kan gøre det manuelt, men det gider vi ikke, så vi får Wordpress til at gøre det for os, ved at indsætte denne funktion i <code>functions.php</code>:</p>

<pre><code class="language-php">function word_count() {
    $content = get_post_field( 'post_content', $post-&gt;ID );
    $word_count = str_word_count( strip_tags( $content ) );
    return $word_count;
}
</code></pre>

<p>Derefter kan antal ord i hver blogindlæg nemt udstilles i <code>dataLayer</code> sådan her:</p>

<pre><code class="language-php">window.dataLayer = window.dataLayer || [];
dataLayer.push({
  'words': '<?php echo word_count(); ?>'
});
</code></pre>

<p>I Google Analytics har jeg så opsat en hit-scoped Custom Dimension, hvor jeg kan opsamle antal ord for blogindlægget, når det bliver besøgt.</p>

<figure><a href="/assets/images/word-count-hit-scoped-custom-dimension.png"><img src="/assets/images/word-count-hit-scoped-custom-dimension.png" alt="Antal ord i hit-scoped custom dimension" width="481" height="330" class="size-full wp-image-1307" /></a><figcaption>Antal ord i hit-scoped custom dimension</figcaption></figure>

<p>Derefter vælger jeg rapporten Acquisition -&gt; All traffic -&gt; Channels og vælger Organic Search. Jeg vælger Landing Page som primær dimension og den nye custom dimension Word Count, som sekundær dimension og får dermed dette udtræk, som eksporteres til Excel.</p>

<figure><a href="/assets/images/word-count-secondary-dimension.png"><img src="/assets/images/word-count-secondary-dimension.png" alt="Datasættet med landingpage, antal ord og organiske sessioner." width="677" height="432" class="size-full wp-image-1308" /></a><figcaption>Datasættet med landingpage, antal ord og organiske sessioner.</figcaption></figure>

<div class="attention"><strong>Bemærk:</strong> Det er vigtigt at vælge en tidsperiode hvor alle blogindlæg har været online i hele perioden. Hvis der kigges tilbage på det sidste år (for at få et godt datagrundlag) men nogle blogindlæg, kun har været online den sidste uge, vil det give et forkert billede på, hvor meget trafik de blogindlæg har fået, sammenlignet med de andre.</div>

<p>Okay, så nu har vi styr på datasættet. Så skal vi igang med noget Machine Learning.</p>

<h2>Der er tre typer af Machine Learning</h2>

<p>Indenfor Machine Learning er der tre primære grupper af analyser du kan lave:</p>

<ul>
    <li><strong>Unsupervised learning:</strong> Kan der findes nogle interessante mønstre i datasættet?</li>
    <li><strong>Supervised learning:</strong> Kan man forudsige noget på baggrund af disse data?</li>
    <li><strong>Reinforcement learning:</strong> Analyse der automatisk bliver klogere af sig selv på baggrund af en række regler, fx, find den optimale strategi i skak</li>
</ul>

<p>I dette indlæg kigger vi på de to første. Lad os starte med den første:</p>

<h2>Unsupervised learning</h2>

<p>Det første skridt er at undersøge om der er en sammenhæng mellem de to variabler. I statistik kaldes de to to variable for den uafhængige variabel (antal ord) og den afhængige variabel (organisk trafik). Vi prøver altså at undersøge om organisk trafik er <em>afhængig</em> af antal ord.</p>

<h3>Er der en sammenhæng? (Scatter Plot)</h3>

<p>Det kan være svært at se om der er en sammenhæng ud fra en tabel med data, så lad os lave lidt simpelt data visualisering med et Scatter Plot.</p>

<p>Hver prik er et blogindlæg og akserne er hhv. antal ord i artikel og organisk trafik til blogindlægget.</p>

<figure><a href="/assets/images/2019/06/machine-learning-organisk-trafik-scatter.png"><img src="/assets/images/2019/06/machine-learning-organisk-trafik-scatter.png" alt="Scatter plot af alle mine indlæg." width="1054" height="601" class="size-full wp-image-1677" /></a><figcaption>Scatter plot af alle mine indlæg.</figcaption></figure>

<p>I første omgang er det svært at se om der er en sammenhæng, da de fleste er klumpet sammen nede i hjørnet. Det skyldes <code>outliers</code>, dvs. observationer i datasættet, som ligger markant uden for normalen. Ekstreme tilfælde er svære at arbejde med, da de vil få alt for stor indflydelse på modellen. Vi ønsker primært at arbejde med data indenfor normal-området.</p>

<p>De to outliers er:</p>

<p>1) Mit indlæg om <a href="/hvor-meget-drikker-gaesterne-til-et-bryllup/">drikkevarer til et bryllup</a>, som får ekstremt meget trafik, sammenlignet med mine normale indlæg om digital marketing, så derfor fjernes den fra modellen.</p>

<p>2) Den anden outlier er mit indlæg om <a href="/responsive-design-3-nemme-trin/">responsivt web design i 3 nemme trin</a> som er på 4253 ord, som er markant længere end mine øvrige indlæg, så derfor fjerner jeg også den.</p>

<figure><a href="/assets/images/2019/06/machine-learning-scatter-outliers.png"><img src="/assets/images/2019/06/machine-learning-scatter-outliers.png" alt="De to outliers der forvrænger data." width="1053" height="597" class="size-full wp-image-1679" /></a><figcaption>De to outliers der forvrænger data.</figcaption></figure>

<p>Nu er det mere tydeligt at se en sammenhæng, hvor blogindlæg med flere ord også har mere organisk trafik.</p>

<figure><a href="/assets/images/2019/06/machine-learning-scatter-outliers-fjernet.png"><img src="/assets/images/2019/06/machine-learning-scatter-outliers-fjernet.png" alt="Scatter plot uden outliers." width="1050" height="596" class="size-full wp-image-1680" /></a><figcaption>Scatter plot uden outliers.</figcaption></figure>

<p>Vi kan gøre det lidt tydeligere, ved at tilføje en tendenslinje.</p>

<figure><a href="/assets/images/2019/06/machine-learning-scatter-tendenslinje.png"><img src="/assets/images/2019/06/machine-learning-scatter-tendenslinje.png" alt="En tendenslinje fremhæver sammenhængen." width="1049" height="599" class="size-full wp-image-1681" /></a><figcaption>En tendenslinje fremhæver sammenhængen.</figcaption></figure>

<p>En tendenslinje gør for det første sammenhængen tydeligere, men den viser også hvor konsistent sammenhængen er i datasættet. Jo tættere alle prikkerne er på linjen, jo mere konsistent er relationen mellem tekstlængden og trafikken. Jo længere prikkerne er fra linjen, jo mere tilfældigt er det og dermed vil dataene typisk ikke være gode at bygge en model på.</p>

<h2>Hvor stærk er sammenhængen? (Korrelation)</h2>

<p>Hvis der er perfekt korrelation, kan man forudsige værdien af den ene variabel, kun ved at kende den anden.</p>

<p>Korrelation udtrykkes som et tal mellem -1 og 1. Jo tættere på yderpunkterne, dvs. -1 eller 1, jo stærkere er korrelationen.</p>

<p>Lad os tage et par eksempler.</p>

<p>Her er først et datasæt med en korrelation på 1, dvs. når den ene stiger, så stiger den anden også.</p>

<figure><a href="/assets/images/2019/06/machine-learning-korrelation-1.00.png"><img src="/assets/images/2019/06/machine-learning-korrelation-1.00.png" alt="Datasæt med korrelation på 1." width="594" height="357" class="size-full wp-image-1682" /></a><figcaption>Datasæt med korrelation på 1.</figcaption></figure>

<p>Det kan for eksempel være personers højde og vægt som ofte følges ad. I den virkelige verden vil man dog typisk aldrig opnå en korrelation på 1,0  da der vil altid være outliers.</p>

<p>Den omvendte er et datasæt med en korrelation på -1, dvs. en negativ korrelation. Det vil altså sige når den ene stiger, så falder en anden.</p>

<figure><a href="/assets/images/2019/06/machine-learning-korrelation-minus-1.00.png"><img src="/assets/images/2019/06/machine-learning-korrelation-minus-1.00.png" alt="Datasæt med korrelation på -1." width="593" height="356" class="size-full wp-image-1687" /></a><figcaption>Datasæt med korrelation på -1.</figcaption></figure>

<p>Det kan fx være sammenhængen mellem hvor koldt det er og hvor mange penge man bruger på at opvarme sit hus.</p>

<p>Det sidste eksempel er et datasæt med en korrelation tæt på 0, dvs. der er ingen sammenhæng mellem de to variabler. Dermed er det umuligt at forudsige hvad den ene værdi vil være hvis man kender den anden værdi.</p>

<figure><a href="/assets/images/2019/06/machine-learning-korrelation-0.03.png"><img src="/assets/images/2019/06/machine-learning-korrelation-0.03.png" alt="Datasæt med korrelation på 0,03." width="593" height="354" class="size-full wp-image-1688" /></a><figcaption>Datasæt med korrelation på 0,03.</figcaption></figure>

<p>Det kan fx være en persons højde og karakter i skolen, hvor der formentlig ikke er nogen som helst sammenhæng, og det er dermed helt tilfældigt hvor prikkerne er placeret, som det ses herover.</p>

<h3>Korrelation mellem antal ord og organisk trafik</h3>

<p>I disse data er korrelationen mellem antal ord og organisk trafik på 0,63.</p>

<figure><a href="/assets/images/2019/06/machine-learning-scatter-tendenslinje.png"><img src="/assets/images/2019/06/machine-learning-scatter-tendenslinje.png" alt="Korrelation på 0,63." width="1049" height="599" class="size-full wp-image-1681" /></a><figcaption>Korrelation på 0,63.</figcaption></figure>

<h3>Årsag eller sammenhæng?</h3>

<p>Okay, så nu har vi fundet ud af at der er en sammenhæng mellem de to variabler. Årsag er en helt anden ting.</p>

<p>Det vi gerne vil undersøge her er nemlig ikke blot om der er en sammenhæng mellem antal ord og organisk trafik, men også om antallet af ord kan forårsage (eller forklare) trafikken og dermed om man kan konkludere at hvis man skriver flere ord, så får man også mere trafik og omvendt.</p>

<p>Når vi snakker årsag så er der tre muligheder:</p>

<ol>
    <li><strong>Antal ord påvirker mængden af organisk trafik.</strong> Det er en sandsynlig forklaring.</li>
    <li><strong>Organisk trafik påvirker antal ord.</strong> Det giver ikke nogen mening. Der kommer ikke flere ord i en artikel, hvis den begynder at modtage mere organisk trafik, så væk med den.</li>
    <li><strong>Både antal ord og organisk trafik bliver påvirket samtidig af en helt 3. faktor.</strong> Den kan også være en sandsynlig forklaring. Forklaringen kan være at årsagen bare er gode velskrevne blogindlæg, som både får meget trafik og at velskrevne blogindlæg også ofte er længere. Dvs. man kan ikke bare skrive længere blogindlæg. Man skal skrive bedre blogindlæg, for at få mere trafik.</li>
</ol>

<p>Ingen kan forklare korrelation bedre end hende her, så lad os give hende 5 minutter og så fortsætter vi derefter.</p>

<div class="videoWrapper">
<iframe width="560" height="315" src="https://www.youtube.com/embed/8B271L3NtAw" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen=""></iframe>
</div>

<h3>Giver det mening?</h3>

<p>Det bedste du kan gøre for at afgøre om den ene variabel forårsager den anden variabel eller der blot er en sammenhæng er at spørge dig selv om det giver mening?</p>

<ul>
    <li>Lange tekster indeholder meget information om et givent emne og er derfor gode at henvise til.</li>
    <li>Lange tekster kommer i bund med et emne.</li>
    <li>Lange tekster dækker mange aspekter af et emne og brugeren skal dermed kun søge information ét sted.</li>
</ul>

<p>Alle sammen er faktorer som gør at Google gerne vil ranke indholdet højt.</p>

<p><a href="http://blog.serpiq.com/how-important-is-content-length-why-data-driven-seo-trumps-guru-opinions/">Tidligere undersøgelser</a> har vist en sammenhæng mellem lange tekster og deres placering i Google.</p>

<h2>Supervised learning</h2>

<p>For at lave en model som kan forudsige organisk trafik baseret på antal ord kan vi lave en regressionsanalyse.</p>

<h2>Regression</h2>

<p>En regressionsanalyse kan laves <a href="https://statisticsbyjim.com/regression/regression-analysis-excel/">direkte i Excel</a> og giver følgende resultat for vores data.</p>

<figure><a href="/assets/images/2019/06/machine-learning-linear-regression.png"><img src="/assets/images/2019/06/machine-learning-linear-regression.png" alt="Resultatet af den linære regression." width="1151" height="446" class="size-full wp-image-1692" /></a><figcaption>Resultatet af den linære regression.</figcaption></figure>

<p>Den blå kasse hedder R squared på engelsk, også kaldet forklaringsgraden. Den viser at 17,5% af variationen i den afhængige variabel (organisk trafik) kan forklares af modellen. Og eftersom der kun er én uafhængig variabel i modellen (antal ord), så kan vi altså sige at antal ord forklarer 17,5% af variationen i organisk trafik. Eller sagt på lidt mere dansk: Antal ord forklarer 17,5% af den organiske trafik et blogindlæg har. Resten af variationen (82,5%) kan ikke forklares af modellen, og skyldes dermed andre faktorer, som er alle de andre ting Google kigger på, når de ranker indhold.</p>

<h2>Hov hov, nu ikke så hurtigt!</h2>

<p>Men husk at vi ikke kan konkludere at antal ord <em><strong>alene</strong></em> forklarer 17,5% af trafikken. Som tidligere nævnt kan det sagtens være en andre ting, som dog korrelerer stærkt med antal ord, fx en velskrevet artikel, som får mange sociale delinger, links, høj CTR, etc.</p>

<h2>Forudsige organisk trafik</h2>

<p>Okay, nu skal vi lige et smut ned af memory lane. Vi skal tilbage til linoleumsgulve og folkeskolens matematiktimer. Kan du huske <em>linjens ligning</em>?</p>

<p>Altså den her: <strong>Y = A * X + B</strong></p>

<p>Hvor A er hældningen på linjen og B er skæringen med Y-aksen. Det er de to tal regressionsanalysen kan udregne for os.</p>

<p>Når vi kander A og B kan vi indsætte antal ord som X og udregne den forventede organisk trafik. Vi skal derfor have linjens ligning for tendenslinjen herunder.</p>

<figure><a href="/assets/images/2019/06/machine-learning-scatter-tendenslinje.png"><img src="/assets/images/2019/06/machine-learning-scatter-tendenslinje.png" alt="Linjens ligning for tendenslinje kan forudsige trafikken." width="1049" height="599" class="size-full wp-image-1681" /></a><figcaption>Linjens ligning for tendenslinje kan forudsige trafikken.</figcaption></figure>

<p>De to tal vi skal bruge til at indsætte istedet for A og B i ligningen får vi altså som en del af resultatet af den linære regression. Det er de to tal i den orange boks.</p>

<figure><a href="/assets/images/2019/06/machine-learning-linear-regression.png"><img src="/assets/images/2019/06/machine-learning-linear-regression.png" alt="De to orange tal er A og B i linjens ligning." width="1151" height="446" class="size-full wp-image-1692" /></a><figcaption>De to orange tal er A og B i linjens ligning.</figcaption></figure>

<p>Det første tal er skæringen med Y-aksen som er -13,95.</p>

<p>Det næste tal er hældningen på linjen som er 0,067.</p>

<p>Dermed bliver linjens ligning: <strong>Y = 0,067 * X - 13,95</strong></p>

<p>Hvis vi fx indsætter X = 5000, får vi følgende: <strong>321,05</strong> = 0,067 * 5000 - 13,95.</p>

<p>Det vil altså sige at den forventede organiske trafik til et blogindlæg på 5000 ord er 321 besøg i samme periode som de øvrige tal er udtrukket for. I dette tilfælde har jeg trukket 1 års besøg ud, så perioden er 1 år.</p>

<p>Hældningen på linjen fortæller desuden at hver gang X stiger med 1, så stiger Y med 0,067.</p>

<p>Sagt på dansk: Hver gang vi skriver et ord mere, så kan vi forvente 0,067 flere besøg om året.</p>]]></content><author><name></name></author><category term="Analytics" /><summary type="html"><![CDATA[Mennesker lærer af erfaring. Maskiner lærer af data.]]></summary></entry><entry><title type="html">Track hvor hurtigt brugeren udfører et event</title><link href="https://www.jacobworsoe.dk/track-hvor-hurtigt-brugeren-udfoerer-et-event/" rel="alternate" type="text/html" title="Track hvor hurtigt brugeren udfører et event" /><published>2021-05-08T16:21:41+00:00</published><updated>2021-05-08T16:21:41+00:00</updated><id>https://www.jacobworsoe.dk/track-hvor-hurtigt-brugeren-udfoerer-et-event</id><content type="html" xml:base="https://www.jacobworsoe.dk/track-hvor-hurtigt-brugeren-udfoerer-et-event/"><![CDATA[<p>En af de ting jeg næsten altid tracker sammen med Event Tracking er time to event.</p>

<p>Dermed kan jeg se hvor lang tid der går fra brugeren lander på siden til et Event udføres.</p>

<p>Det tilføjer også en spændende dimension til dine data, som ikke er tilgængelig i en standard Google Analytics opsætning, nemlig tid mellem forskellige events indenfor en session.</p>

<p>Google Analytics er god til at vise hvor mange sessioner, transaktioner, etc. du har haft på en given dag eller time, samt den gennemsnitlige varighed for de sessioner. Men der er ikke noget data på rækkefølgen af events eller tiden mellem de enkelte events. Det tilføjer vi noget af i dag.</p>

<p>Her viser jeg hvordan du sætter det op og eksempler på hvordan det kan forbedre dine analyser.</p>

<h2>Opsætning i Google Tag Manager</h2>

<p>I Google Tag Manager skal der laves to ting:</p>

<ul>
<li>En Custom JS variabel som udregner tiden</li>
<li>Et Event Tag hvor resultatet af variablen indsætter som Værdi.</li>
</ul>

<h3>Udregn hvor hurtigt et event udføres</h3>

<p>For at udregne hvor hurtigt et Event udføres, skal der bruges to tal.</p>

<ol>
<li>Et præcist tidspunkt når Event'et udføres.</li>
<li>Et præcist tidspunkt når siden indlæses.</li>
</ol>

<p>Når man skal regne i tid er Unix timestamps geniale. Unix timestamp er den tid der er gået siden 1. januar 1970 - typisk i antal sekunder eller millisekunder.</p>

<p>Og det er super nemt at få det timestamp i millisekunder med JavaScript.</p>

<pre><code class="language-javascript">new Date().getTime()
</code></pre>

<p>Men det har jeg det præcise tidspunkt for hvornår et Event sker.</p>

<p>Så skal jeg bare have tidspunktet hvor siden blev indlæst.</p>

<pre><code class="language-javascript">window.performance.timing.navigationStart
</code></pre>

<p>Ved at trække de to tal fra hinanden, ved du præcist (i millisekuder) hvor lang tid der er gået fra brugeren begyndte at loade siden, til et givent Event bliver udført.</p>

<p>Du laver derfor en Custom JavaScript Variable i GTM, hvor du trækker de to tal fra hinanden. Husk at en Custom JavaScript Variable altid skal være en <code>function</code> som returnerer en værdi.</p>

<p>Funktionen gør følgende pr. linje:</p>

<ol>
<li>Gemmer tiden lige nu i millisekunder</li>
<li>Trækker tiden da siden blev indlæst fra tiden nu i millisekunder. Dividerer med 1000 for at omregne til sekunder</li>
<li>Returnerer tiden hvis den er under 1800 sekunder. Hvis den er over returneres `undefined`</li>
</ol>

<p>Jeg tjekker om der er gået mere end 1800 sekunder (30 minutter) siden dengang siden blev indlæst, for at undgå at tracke nogle meget høje antal sekunder, som vil skævvride data markant. Hvis der er gået mere end 30 minutter er der stor sandsynlighed for at sessionen er udløbet, så det er en god grænse at sætte.</p>

<p>Det smarte ved at returnere undefined som værdi er at <a href="https://www.simoahava.com/gtm-tips/undefined-dimensions-wont-get-sent/" rel="noopener noreferrer">undefined værdier automatisk bliver udeladt</a> af dit request til Google Analytics.</p>

<pre><code class="language-javascript">function() {
  var currentTime = new Date().getTime();
  var timeToEvent = (currentTime - window.performance.timing.navigationStart)/1000;
  return timeToEvent &lt; 1800 ? timeToEvent : undefined;
}
</code></pre>

<p>Jeg bruger altid den samme navngivning når jeg opretter Tags, Variabler eller Triggers i GTM. Fx starter jeg altid en Custom JavaScript Variable med “JS - [navn]” så jeg har dem samlet i oversigten og de er nemme at finde.</p>

<p>Den her kalder jeg “JS - Time to event”.</p>

<figure><a href="/assets/images/2019/10/GTM-variabel-JS-time-to-event.jpg"><img src="/assets/images/2019/10/GTM-variabel-JS-time-to-event-725x337.jpg" alt="GTM variabel: JS - Time to event" width="725" height="337" class="size-large wp-image-1981" /></a><figcaption>GTM variabel: JS - Time to event</figcaption></figure>

<h2>Event Tag med tiden som værdi</h2>

<p>Du kan derefter indsætte den nye variabel som Værdi i dine Event tracking Tags.</p>

<figure><a href="/assets/images/2019/10/GTM-tag-JavaScript-error-configuration.jpg"><img src="/assets/images/2019/10/GTM-tag-JavaScript-error-configuration.jpg" alt="Indsæt &quot;JS - time to event&quot; som Event Value." width="740" height="702" class="size-full wp-image-1980" /></a><figcaption>Indsæt "JS - time to event" som Event Value.</figcaption></figure>

<p>Det fede ved at tracke tiden som værdien for et Event er at Google Analytics udregner et gennemsnit ud-af-boksen. Dermed får du et hurtigt overblik over hvor hurtigt et Event sker i gennemsnit.</p>

<h3>Use case #1: JavaScript fejl</h3>

<p>En af de ting jeg altid sætter op i GTM er tracking af JavaScript fejl. Det er mega nemt og værdifuldt, fordi det er indbygget i GTM, så der skal ikke kodes noget.</p>

<figure><a href="/assets/images/2019/10/JavaScript-error-events-with-time.jpg"><img src="/assets/images/2019/10/JavaScript-error-events-with-time-725x352.jpg" alt="JavaScript error event med tiden som værdi." width="725" height="352" class="size-large wp-image-1982" /></a><figcaption>JavaScript error event med tiden som værdi.</figcaption></figure>

<p>Ved at tracke tiden indtil der sker en JavaScript fejl, er det meget nemt at se hvilke fejl der sker ved pageload og hvilke der først sker efter brugeren er begyndt at bruge siden. Dét gør det tit meget nemmere at debugge en JavaScript fejl og forstå hvad der gik galt.</p>

<h3>Use case #2: Læsning af indhold</h3>

<p>Jeg tracker min blog med Enhanced Ecommerce, som du kan <a href="/indhold-enhanced-ecommerce/">læse meget mere om her</a>. Jeg tracker en række events når brugeren har læst hhv. 33%, 66% samt 100% af et blogindlæg. Her bruger jeg også tiden, til at se hvor hurtigt brugerne læser indholdet og hvor lang tid det tager dem at nå til bunden af siden.</p>

<figure><a href="/assets/images/2019/10/Content-with-ecommerce-time-to-events.jpg"><img src="/assets/images/2019/10/Content-with-ecommerce-time-to-events-860x313.jpg" alt="Den gennemsnitlige tid det tager brugerne af scrolle igennem et blogindlæg." width="860" height="313" class="size-large wp-image-1984" /></a><figcaption>Den gennemsnitlige tid det tager brugerne af scrolle igennem et blogindlæg.</figcaption></figure>

<p>Jeg tracker et “add to cart” event, når brugeren begynder at scrolle ned af indlægget. Det er overraskende at det igennemsnit tager 26,52 sekunder før brugerne begynder at scrolle, men det tager 51,97 sekunder at nå 33% ned gennem indlægget (checkout step 1).</p>

<h3>Use case #3: Klik i menuen</h3>

<p>Mange websites tracker hvad brugeren klikker på i menuen. Særligt i en mega-menu kan det være nyttigt at vide hvor brugerne klikker og hvilke områder der ikke modtager nogen kliks. Her kan tiden også bidrage med ekstra viden, til at se hvor hurtigt brugeren kan danne sig et overblik over menuen og foretage et valg.</p>

<p>Her på bloggen har jeg en hamburger menu, hvor jeg tracker når den åbnes og lukkes.</p>

<figure><a href="/assets/images/2019/10/Hamburger-menu-open-close.jpg"><img src="/assets/images/2019/10/Hamburger-menu-open-close-860x246.jpg" alt="I gennemsnit åbnes hamburger menuen efter 116 sekunder og lukkes efter 147 sekunder." width="860" height="246" class="size-large wp-image-1986" /></a><figcaption>I gennemsnit åbnes hamburger menuen efter 116 sekunder og lukkes efter 147 sekunder.</figcaption></figure>

<p>Jeg tracker også når brugeren klikker i menuen. Her er det interessant at se hvor stor forskel der er på hvor hurtigt brugerne klikker på de forskellige kategorier af indlæg. Bemærk at dette er tiden efter siden blev indlæst og ikke tiden efter menuen blev åbnet.</p>

<figure><a href="/assets/images/2019/10/Klik-i-hamburger-menuen.jpg"><img src="/assets/images/2019/10/Klik-i-hamburger-menuen-860x499.jpg" alt="Tid før klik på de forskellige kategorier i menuen." width="860" height="499" class="size-large wp-image-1987" /></a><figcaption>Tid før klik på de forskellige kategorier i menuen.</figcaption></figure>

<p>Jeg har efterfølgende lavet et stort redesign af min blog og alle beslutningerne var baseret på data, fx at fjerne menuen. <a href="/datadrevet-redesign/">Det kan du læse mere om her</a>.</p>

<h3>Use case #4: Brug af drikkevare beregner</h3>

<p>Tilbage i 2015 lavede jeg en <a href="/hvor-meget-drikker-gaesterne-til-et-bryllup/">infografik over hvor meget der blev drukket til vores bryllup</a>. Det er i øvrigt suverænt det mest blogindlæg jeg har. Jeg skal have skrevet noget mere/bedre om Google Analytics.</p>

<p><a href="/assets/images/2019/10/Mest-bes%C3%B8gte-indl%C3%A6g-siden-2009.jpg"><img src="/assets/images/2019/10/Mest-bes%C3%B8gte-indl%C3%A6g-siden-2009.jpg" alt="" width="844" height="465" class="alignnone size-full wp-image-1993" /></a></p>

<p>Jeg lavede også en beregner hvor man kan indtaste antal gæster og få at vide hvor meget man skal købe.</p>

<p><a href="/assets/images/2019/10/Drikkevare-beregner-50-g%C3%A6ster.jpg"><img src="/assets/images/2019/10/Drikkevare-beregner-50-g%C3%A6ster-860x516.jpg" alt="" width="860" height="516" class="alignnone size-large wp-image-1995" /></a></p>

<p>Der er lavet 18.555 beregninger til dato og der er i gennemsnit gået 110 sekunder fra siden blev indlæst, til der er udført en beregning.</p>

<p><a href="/assets/images/2019/10/Drikkevare-beregner-gns-tid.jpg"><img src="/assets/images/2019/10/Drikkevare-beregner-gns-tid-860x161.jpg" alt="" width="860" height="161" class="alignnone size-large wp-image-1996" /></a></p>

<p>Bounteous har også skrevet en fin artikel <a href="https://www.bounteous.com/insights/2018/02/06/average-time-until-event-calculated-metrics/?ns=l">om at tracke tiden op til et event</a> hvis du vil læse mere om emnet.</p>]]></content><author><name></name></author><category term="Analytics" /><summary type="html"><![CDATA[En af de ting jeg næsten altid tracker sammen med Event Tracking er time to event.]]></summary></entry><entry><title type="html">Hvilken side ser brugerne efter forsiden?</title><link href="https://www.jacobworsoe.dk/hvilken-side-ser-brugerne-efter-forsiden/" rel="alternate" type="text/html" title="Hvilken side ser brugerne efter forsiden?" /><published>2021-04-03T20:41:20+00:00</published><updated>2021-04-03T20:41:20+00:00</updated><id>https://www.jacobworsoe.dk/hvilken-side-ser-brugerne-efter-forsiden</id><content type="html" xml:base="https://www.jacobworsoe.dk/hvilken-side-ser-brugerne-efter-forsiden/"><![CDATA[<p>Et af de spørgsmål som mange stiller er:</p>

<blockquote><p>Kan vi se hvad brugerne gør efter de ser forsiden?</p></blockquote>

<p>Det er et godt spørgsmål!</p>

<p>Men umiddelbart ikke så nemt at svare på…</p>

<h2>Problemet med Next Page Path</h2>

<p>Problemet er at der findes ikke en next path dimension.</p>

<p>Eller joh, det gør der. Den hedder Next Page Path. Men du får ikke det der står udenpå æsken.</p>

<p>Den giver bare det samme som Page dimensionen.</p>

<p>Det skyldes at et givent hit eller pageview i Google Analytics, isoleret set, ikke har informationer om hvad der sker efterfølgende. Man kan derfor ikke vælge forsiden i “All pages”-rapporten og derefter se hvad brugerne gør derefter med <code>Next Page Path</code> dimensionen. Den indeholder nemlig præcis det samme som <code>Page</code> dimensionen.</p>

<p><a href="/assets/images/2019/11/Page-og-Next-Page-Path.jpg"><img src="/assets/images/2019/11/Page-og-Next-Page-Path.jpg" alt="" width="812" height="390" class="alignnone size-full wp-image-2132" /></a></p>

<p>Next Page Path er misvisende, hvilket også er grunden til at Google Analytics har gjort den deprecated og fjerner den på et tidspunkt.</p>

<p><a href="/assets/images/2019/11/Next-page-deprecated.jpg"><img src="/assets/images/2019/11/Next-page-deprecated.jpg" alt="" width="826" height="99" class="alignnone size-full wp-image-2133" /></a></p>

<p>Du får også en advarsel hvis du bruger den i en custom report.</p>

<p><a href="/assets/images/2019/11/Next-page-deprecated-warning.jpg"><img src="/assets/images/2019/11/Next-page-deprecated-warning-860x311.jpg" alt="" width="860" height="311" class="alignnone size-large wp-image-2134" /></a></p>

<h2>Previous Page er løsningen</h2>

<p>Til gengæld kan man altid se den forrige side, via <code>document.referrer</code> som bliver tracket i dimensionen Previous Page Path.</p>

<p><a href="/assets/images/2019/11/document.referrer.jpg"><img src="/assets/images/2019/11/document.referrer.jpg" alt="" width="567" height="151" class="alignnone size-full wp-image-2135" /></a></p>

<p>Tricket til at se den næste side efter forsiden er derfor at lave en rapport som kigger på alle sider og filtrere så du kun ser de sider hvor den <em>forrige</em> side er forsiden. Det kan nemt laves i en custom report sådan her:</p>

<figure><a href="/assets/images/2019/11/custom-report-previous-page-og-page-settings.jpg"><img src="/assets/images/2019/11/custom-report-previous-page-og-page-settings-860x285.jpg" alt="Custom Report filtreret så Previous Page er forsiden." width="860" height="285" class="size-large wp-image-2138" /></a><figcaption>Custom Report filtreret så Previous Page er forsiden.</figcaption></figure>

<p>Og dermed får du den ønskede oversigt:</p>

<p><a href="/assets/images/2019/11/custom-report-previous-page-og-page-view.jpg"><img src="/assets/images/2019/11/custom-report-previous-page-og-page-view-860x356.jpg" alt="" width="860" height="356" class="alignnone size-large wp-image-2137" /></a></p>]]></content><author><name></name></author><category term="Analytics" /><summary type="html"><![CDATA[Et af de spørgsmål som mange stiller er:]]></summary></entry><entry><title type="html">Ødelægger en slide-in konverteringsraten?</title><link href="https://www.jacobworsoe.dk/slide-in-konverteringsrate/" rel="alternate" type="text/html" title="Ødelægger en slide-in konverteringsraten?" /><published>2020-09-13T21:17:02+00:00</published><updated>2020-09-13T21:17:02+00:00</updated><id>https://www.jacobworsoe.dk/slide-in-konverteringsrate</id><content type="html" xml:base="https://www.jacobworsoe.dk/slide-in-konverteringsrate/"><![CDATA[<p>Slide-ins er geniale. Altså sådan nogle som danske <a href="https://sleeknote.com/">Sleeknote</a> laver. Eller sagt på godt gammeldags dansk: En <a href="https://trendsonline.dk/2014/06/10/twami-fusioneres-og-bliver-til-sleeknote/">twami</a> :)</p>

<p>De er geniale til at indsamle e-mail permission på et website. Og de er ikke så irriterende som pop-ups.</p>

<p>Eller er de?</p>

<p>Kan de være så irriterende at brugerne ikke konverterer og måske forlader sitet?</p>

<p>Det har jeg testet!</p>

<h2>Indhold</h2>
<ul>
  <li><a href="#article-header-id-0">Byg en simpel slide-in</a></li>
    <ul><li><a href="#article-header-id-1">HTML</a></li>
    <li><a href="#article-header-id-2">CSS</a></li>
    <li><a href="#article-header-id-3">JavaScript</a></li></ul>
  <li><a href="#article-header-id-4">Hvornår skal den så vises?</a></li>
    <ul><li><a href="#article-header-id-5">Vis på et tilfældigt tidspunkt</a></li></ul>
  <li><a href="#article-header-id-6">Effekt på tilmelding til nyhedsbrev</a></li>
  <li><a href="#article-header-id-7">Effekt på læsning af blogindlæg</a></li>
    <ul><li><a href="#article-header-id-8">Læst 33%</a></li>
    <li><a href="#article-header-id-9">Læst 66%</a></li>
    <li><a href="#article-header-id-10">Læst 100%</a></li>
    <li><a href="#article-header-id-11">Læst 100% + 1 minut</a></li></ul>
  <li><a href="#article-header-id-12">Opsummering</a></li>
</ul>

<h2 id="article-header-id-0">Byg en simpel slide-in</h2>

<p>Jeg lavede en simpel slide-in her på min blog.</p>

<h3 id="article-header-id-1">HTML</h3>

<p>Selve slide-in boxen er blot noget simpel HTML.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"slide-in-box"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"#"</span> <span class="na">class=</span><span class="s">"slide-in-box-close"</span><span class="nt">&gt;&lt;/a&gt;</span>
    <span class="nt">&lt;h3&gt;</span>Få gratis tips og tricks i din indbakke!<span class="nt">&lt;/h3&gt;</span>
    <span class="nt">&lt;form</span> <span class="na">action=</span><span class="s">""</span> <span class="na">method=</span><span class="s">"post"</span><span class="nt">&gt;</span>
    ... input felter ...
    <span class="nt">&lt;/form&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>

<h3 id="article-header-id-2">CSS</h3>

<p>Den kan styles på mange måder, men det vigtigste er at den har <code>position: fixed</code> så den er låst i bunden af skærmen og <code>z-index: 99999</code> så den er “ovenpå” websitet. Derudover har den <code>bottom: -100%</code> som gør at den som standard er skjult “under” skærmen.</p>

<p>Når slide-in boxen skal vises, sætter jeg en <code>.open</code> class på den med JavaScript, som sætter <code>bottom: 5px</code> som gør at den bliver synlig 5px over bunden af skærmen.</p>

<p><code>transition: .5s bottom ease-in-out</code> tilføjer en animation når værdien af <code>bottom</code> ændres, som gør at den “slider” op nedefra.</p>

<pre><code class="language-sass">.slide-in-box  
  z-index: 99999  
  position: fixed 
  bottom: -100% 
  transition: .5s bottom ease-in-out

  &amp;.open
    bottom: 5px
</code></pre>

<p>Dermed har jeg en simpel slide-in box.</p>

<figure><img src="/assets/images/2020/09/slide-in-box.jpg" alt="Den færdige slide-in box." width="447" height="450" class="size-full wp-image-2510 no-border" /><figcaption>Den færdige slide-in box.</figcaption></figure>

<h3 id="article-header-id-3">JavaScript</h3>

<p>Det sidste er et stykke JavaScript til at vise boxen.</p>

<p>Slide-in boxen skal kunne åbne på forskellige tidspunkter, så jeg har lavet en funktion, som jeg kan kalde til at åbne den, som gør følgende.</p>

<ol>
<li>Funktionen tager en parameter <code>randomPageState</code> som er tidspunktet hvor den skal åbne. Det kommer vi tilbage til lige om lidt.</li>
<li>Hvis personen tidligere har set og lukket en slide-in box, bliver det gemt i <code>localStorage</code>. Jeg tjekker om det er sket, så jeg ikke viser boxen igen til samme bruger.</li>
<li>Jeg sætter en <code>.open</code> class på boxen så den åbner.</li>
<li>Til sidst tracker jeg et event med at boxen er åbnet og tidspunktet den åbnede på.</li>
</ol>

<pre><code class="language-javascript">showSlideUpBox: function(randomPageState) {    
    var slideInOptOut = localStorage.getItem("slideInOptOut");
    var slideUpBox = document.querySelector(".slide-in-box");
    if (!slideInOptOut) {
      slideUpBox.classList.add("open");
      Tracking.trackSlideUpBox("open", randomPageState);
    }
}
</code></pre>

<p>Sådan ser det ud når slide-in boxen bliver synlig.</p>

<figure><img src="/assets/images/2020/09/Slide-in-16c-lossy0.gif" alt="Slide-in boxen bliver synlig." width="830" height="467" class="size-full wp-image-2511" /><figcaption>Slide-in boxen bliver synlig.</figcaption></figure>

<h2 id="article-header-id-4">Hvornår skal den så vises?</h2>

<p>Jeg tracker detaljeret hvor meget der bliver læst af mine blogindlæg ved at sende et event på følgende tidspunkter.</p>

<ol>
<li>Når brugeren lander på et blogindlæg (detail view)</li>
<li>Når brugeren begynder at scrolle (add to cart).</li>
<li>Når brugeren har scrollet 33% af indlæggets længde (checkout step 1).</li>
<li>Når brugeren har scrollet 66% af indlæggets længde (checkout step 2).</li>
<li>Når brugeren har scrollet 100% af indlæggets længde (checkout step 3).</li>
<li>Når brugeren har scrollet 100% af indlæggets længde og været på siden mindst et minut (purchase).</li>
</ol>

<p>Jeg tracker de events med <a href="/indhold-enhanced-ecommerce/">Enhanced Ecommerce hvis du vil læse mere om det</a>.</p>

<p>Jeg vil gerne tracke det optimale tidspunkt at vise en slide-in på af de 6 ovenstående tidspunkter. Både for at øge konverteringen og få flest mulige tilmeldinger til mit <a href="/nyhedsbrev/">nyhedsbrev</a>. Men også for at undgå at irritere brugeren, så de forlader siden uden at læse indlægget færdigt.</p>

<h3 id="article-header-id-5">Vis på et tilfældigt tidspunkt</h3>

<p>Ud af de 6 ovenstående tidspunkter jeg kan vise en slide-in på, vil jeg dog ikke vise den med det samme når brugeren lander på siden, så jeg vil teste de andre 5 tidspunkter.</p>

<p>Jeg har lavet et array med de 5 tidspunkter:</p>

<pre><code class="language-javascript">var randomPageStates = ["scroller","oneThird","twoThirds","endContent","purchase"];
</code></pre>

<p>Når brugeren lander på et blogindlæg bruger jeg JavaScript til at generere et tilfældigt tal mellem 0-4.</p>

<pre><code class="language-javascript">var randomPageState = Math.floor(Math.random() * 5);
</code></pre>

<p>Dette tal bruger jeg så til at vælge et af de 5 tidspunkter. Arrays i JavaScript er 0-indexeret, så den første plads er index 0, op til index 4.</p>

<p>Jeg bruger det tilfældige tal til at vælge et tilfældigt index i array’et, som styrer hvornår min slide-in vises på siden. Jeg tracker også værdien i en Hit-scoped custom dimension, så jeg kan sammenholde det med konvertering og frafald.</p>

<p>Lad os starte med konverteringer.</p>

<h2 id="article-header-id-6">Effekt på tilmelding til nyhedsbrev</h2>

<p>Okay, vi kan lige så godt tage den med det samme. Min slide-in konverterer ikke. Slet ikke.</p>

<figure><img src="/assets/images/2020/09/Slide-in-conversion-events-dark-860x435.jpg" alt="22.161 visninger af slide-in og kun 29 tilmeldinger." width="860" height="435" class="size-large wp-image-2540" /><figcaption>22.161 visninger af slide-in og kun 29 tilmeldinger.</figcaption></figure>

<p>Slide-in boxen er i alt blevet vist 22.161 gange og den er blevet lukket 15.490 gange. Men den har kun fået 29 tilmeldinger.</p>

<p>29!</p>

<p>Det er en konverteringsrate på 0,13%.</p>

<p>Men det er også min egen skyld. Jeg sælger slet ikke den tilmelding godt nok. Det er ikke nok bare at give mulighed for at få en mail ved nye blogindlæg.</p>

<p>Det virker meget bedre med en <a href="http://pottercut.dk/emil-kristensen/faa-flere-tilmeldinger-din-nyhedsmail/">Content Upgrade</a>.</p>

<p>Nåh, men det var heller ikke det vi skulle snakke om!</p>

<h2 id="article-header-id-7">Effekt på læsning af blogindlæg</h2>

<p>Jeg vil gerne undersøge om brugerne er mere tilbøjelige til at forlade sitet når min slide-in bliver vist.</p>

<p>I det efterfølgende kigger jeg på konverteringsraten for de enkelte events ned gennem et blogindlæg sammenlignet med detail views som er det samme som sidevisninger på blogindlægget.</p>

<p>Det første jeg kigger på er Add to Cart konverteringsraten, dvs. hvor mange der begynder at scrolle.</p>

<p>Denne er lidt ligeyldig fordi min slide-in tidligst bliver vist på det tidspunkt hvor de rammer Add to Cart. Så den kan faktisk ikke nå at påvirke, men den er god at have med som kontrol.</p>

<p>De fem søjler er de fem tidspunkter slide-in boxen bliver vist på. Ikke overraskende er konverteringsraten næsten den samme for alle grupper.</p>

<figure><img src="/assets/images/2020/09/Add-to-Cart-konvertering.jpg" alt="Ikke overraskende er konverteringsraten næsten den samme for alle grupper." width="1035" height="578" class="size-full wp-image-2530" /><figcaption>Ikke overraskende er konverteringsraten næsten den samme for alle grupper.</figcaption></figure>

<h3 id="article-header-id-8">Læst 33%</h3>

<p>Det næste event er dem som har scrollet 33% af indlægget og her vil den første gruppe altså have set slide-in boxen, mens de andre ikke har set den endnu.</p>

<figure><img src="/assets/images/2020/09/Laest-33p-konvertering.jpg" alt="Ikke den store forskel på dem der har set slide-in boxen." width="1033" height="580" class="size-full wp-image-2531" /><figcaption>Ikke den store forskel på dem der har set slide-in boxen.</figcaption></figure>

<p>Add to Cart har en anelse lavere konverteringsrate end Læst 33% men forskellen er meget lille og med en p-værdi på 0,1572 er den ikke signifikant.</p>

<figure><a href="https://www.surveymonkey.com/mp/ab-testing-significance-calculator/"><img src="/assets/images/2020/09/significance-test-860x413.jpg" alt="SurveyMonkey: A/B-test signifikansberegner (skærmbillede)" width="860" height="413" class="size-large wp-image-2543" /></a><figcaption><a href="https://www.surveymonkey.com/mp/ab-testing-significance-calculator/">SurveyMonkey</a> har en god signifikans test.</figcaption></figure>

<p>Man kan også se at Læst 100% har en konverteringsrate der er endnu lavere end Add to Cart og Læst 100% har altså ikke set slide-in boxen endnu, så det er bare naturlig varians.</p>

<p>Indtil videre påvirker det altså ikke konverteringsraten.</p>

<h3 id="article-header-id-9">Læst 66%</h3>

<p>Ved 66% læst er der heller ingen markant forskel og faktisk er det de to øverste grupper (som på dette tidspunkt har set slide-in boxen) der har den højeste konvertering.</p>

<figure><img src="/assets/images/2020/09/Laest-66p-konvertering.jpg" alt="Ved 66% læst er der heller ikke forskel." width="1032" height="578" class="size-full wp-image-2532" /><figcaption>Ved 66% læst er der heller ikke forskel.</figcaption></figure>

<p>Så stadig ingen påvirkning.</p>

<h3 id="article-header-id-10">Læst 100%</h3>

<p>Ved 100% læst er det dermed de øverste 3 grupper der har fået vist slide-in boxen - men det har stadig ingen betydning.</p>

<figure><img src="/assets/images/2020/09/Laest-100p-konvertering.jpg" alt="Samme billede ved 100% læst." width="1030" height="577" class="size-full wp-image-2533" /><figcaption>Samme billede ved 100% læst.</figcaption></figure>

<p>Videre til sidste event.</p>

<h3 id="article-header-id-11">Læst 100% + 1 minut</h3>

<p>Det sidste event er dem som har scrollet 100% og været på siden mindst 1 minut - det er dem hvor jeg antager at de faktisk har læst hele blogindlægget (<a href="/indhold-enhanced-ecommerce/">og dem jeg tracker som et transaktion i Enhanced Ecommerce</a>).</p>

<figure><img src="/assets/images/2020/09/Laest-100p-1-minut-konvertering.jpg" alt="Heller ikke ved 100% læst + 1 minut på siden er der nogen entydig forskel." width="1030" height="577" class="size-full wp-image-2534" /><figcaption>Heller ikke ved 100% læst + 1 minut på siden er der nogen entydig forskel.</figcaption></figure>

<p>Men heller ikke her er der nogen entydig forskel som skulle indikere at brugerne bliver irriteret af en slide-in og dermed er mere tilbøjelige til at forlade sitet eller mindre tilbøjelige til at læse blogindlægget færdig.</p>

<p>Konklusionen er derfor at min slide-in ikke er irriterende for brugerne - i hvert fald ikke så meget at de ikke gider læse blogindlægget færdigt.</p>

<h2 id="article-header-id-12">Opsummering</h2>

<p>I dette blogindlæg har jeg brugt Google Analytics og Google Tag Manager til at indsamle det nødvendige data til at teste hypotesen om at slide-ins ødelægger konverteringsraten. Min konklusion gælder ikke for alle sites og alle brancher - sådan noget skal altid testes på det enkelte site. Men jeg håber at ovenstående kan give noget inspiration til hvordan man kan opstille en hypotese, indsamle den nødvendige data og analysere det, så der kan træffes en beslutning på baggrund af data.</p>

<p>Tilbage er der blot at få lavet en ordentlig content upgrade, så jeg kan lave en slide-in der faktisk kan give nogle tilmeldinger til mit <a href="/nyhedsbrev/">nyhedsbrev</a>.</p>]]></content><author><name></name></author><category term="Analytics" /><summary type="html"><![CDATA[Slide-ins er geniale. Altså sådan nogle som danske Sleeknote laver. Eller sagt på godt gammeldags dansk: En twami :)]]></summary></entry><entry><title type="html">Datadrevet redesign</title><link href="https://www.jacobworsoe.dk/datadrevet-redesign/" rel="alternate" type="text/html" title="Datadrevet redesign" /><published>2020-08-15T22:59:24+00:00</published><updated>2020-08-15T22:59:24+00:00</updated><id>https://www.jacobworsoe.dk/datadrevet-redesign</id><content type="html" xml:base="https://www.jacobworsoe.dk/datadrevet-redesign/"><![CDATA[<p>Jeg startede denne blog i 2009 og <a href="/design-versioner/" rel="noopener">den første version</a> var bygget helt fra bunden hvor jeg havde kodet det hele i PHP med MySQL som database.</p>

<p>I 2013 <a href="/flyttet-til-wordpress/" rel="noopener noreferrer">flyttede jeg bloggen over på WordPress</a> hvor alt HTML blev konverteret til WordPress templates, men designet forblev nogenlunde uændret.</p>

<p>I 2014 <a href="/responsive-design-3-nemme-trin/">lavede jeg designet responsivt</a> så siden blev mobilvenlig.</p>

<p>I 2019 var det så tid til et redesign hvor jeg har brugt data fra Google Analytics, både til at tage beslutninger for at få sitet til at loade så hurtigt som muligt, men også til at forbedre KPI’erne for sitet. Jeg har også testet et råd fra Steve Krug’s Dont Make Me Think, for at få brugerne til at læse flere af mine blogindlæg.</p>

<p>Jeg har delt blogindlægget op i to dele, hvor den første del fokuserer på hvordan jeg har kodet sitet (der er brugt GA data to steder, som er markeret med fed herunder).</p>

<p>Den anden del fokuserer på optimering med Google Analytics og opfølgning på effekten af de ændringer jeg har lavet.</p>

<h2>Indhold</h2>

<p><strong>Part 1:</strong> Det tekniske med fokus på hvordan designet er kodet.</p>

<ul>
<li><a href="#article-header-id-0">Mål med det nye design</a></li>
<li><a href="#article-header-id-1">Workflow</a>

<ul>
<li><a href="#article-header-id-11">GruntJS</a></li>
<li><a href="#article-header-id-2">Kun én JavaScript fil</a></li>
<li><a href="#article-header-id-3">Browser caching og cache busting</a></li>
<li><a href="#article-header-id-4">Gruntfile.js</a></li>
</ul></li>
<li><a href="#article-header-id-5">JavaScript</a>

<ul>
<li><a href="#article-header-id-5">Tracking logik fra GTM til dataLayer</a></li>
<li><a href="#article-header-id-6">Væk med jQuery</a></li>
<li><a href="#article-header-id-7">JavaScript reduceret fra 184 KB til 31 KB</a></li>
</ul></li>
<li><a href="#article-header-id-8">CSS</a>

<ul>
<li><a href="#article-header-id-81">CSS skrevet i Sass</a></li>
<li><a href="#article-header-id-9">Inline CSS eller ekstern fil? <strong>(Baseret på adfærdsdata fra Google Analytics)</strong></a></li>
<li><a href="#article-header-id-10">Inline kun den nødvendige CSS kode</a></li>
<li><a href="#article-header-id-11">CSS reduceret med 45%</a></li>
<li><a href="#article-header-id-12">Væk med !important</a></li>
</ul></li>
<li><a href="#article-header-id-13">Billeder</a></li>
<li><a href="#article-header-id-14">WordPress</a>

<ul>
<li><a href="#article-header-id-141">Væk med unødvendige plugins</a></li>
<li><a href="#article-header-id-15">Tabeller</a></li>
<li><a href="#article-header-id-16">Syntax highlighting</a></li>
<li><a href="#article-header-id-17">Relaterede indlæg <strong>(Baseret på adfærdsdata fra Google Analytics)</strong></a></li>
</ul></li>
</ul>

<p><strong>Part 2:</strong> Optimering af adfærden på sitet med Google Analytics data.</p>

<ul>
<li><a href="#article-header-id-18">Loadtid og konvertering</a>

<ul>
<li><a href="#article-header-id-181">Google Pagespeed Score hævet fra 86 til 99</a></li>
<li><a href="#article-header-id-19">Er sitet så blevet hurtigere?</a></li>
<li><a href="#article-header-id-20">Hvad med konvertering?</a></li>
<li><a href="#article-header-id-21">Øge sidevisninger pr. besøg (test af råd fra Steve Krug)</a></li>
</ul></li>
<li><a href="#article-header-id-22">Lykkedes målene?</a></li>
</ul>

<h2 id="article-header-id-0">Mål med det nye design</h2>

<ol>
    <li>Mere moderne workflow til udvikling af websitet</li>
    <li>Oprydning i kode og sletning af unødvendige ting</li>
    <li>Hurtigere loadtid</li>
    <li>Højere konvertering</li>
    <li>Øge sidevisninger pr. besøg</li>
</ol>

<h2 id="article-header-id-1">Workflow</h2>

<h3 id="article-header-id-11">GruntJS</h3>

<p>Der er mange (kedelige) opgaver involveret i at optimere front-end kode og jeg bruger <a href="https://gruntjs.com/">GruntJS</a> som task runner, til at udføre alle de opgaver automatisk.</p>

<p>GruntJS gør følgende ved mine filer:</p>

<ol>
<li>Samler JavaScript filer til én samlet, minificeret fil. Både de forskellige libraries jeg bruger og mine egne JavaScript filer.</li>
<li>Compiler alle <a href="https://sass-lang.com/">Sass</a> filerne til en minificeret CSS fil.</li>
<li>Kopiere alle de færdige optimerede filer over i en mappe, som indeholder de filer der skal uploades til webserveren.</li>
<li>JavaScript filen bliver cachet 1 år i browseren og derfor får den et unikt nyt navn, hvis filen ændres, så browseren downloader den nye fil.</li>
<li>Grunt overvåger mine filer og kører de ovenstående opgaver, når jeg gemmer en ny ændring, fx i en Sass eller JavaScript fil.</li>
</ol>

<h3 id="article-header-id-2">Kun én JavaScript fil</h3>

<p>Det er ikke så vigtigt efter HTTP2 blev lanceret, men det er stadig best practise at lave så få requests som muligt, fx ved at samle alt JavaScript i én fil. Takket være GruntJS bliver dette gjort helt automatisk og derefter bliver filen minificeret.</p>

<h3 id="article-header-id-3">Browser caching og cache busting</h3>

<p>JS filen caches i browseren i 1 år ved at sætte expire-headers til 1 år i <code>.htaccess</code>. Jeg har brugt de anbefalede settings fra <a href="https://html5boilerplate.com/" rel="noopener noreferrer" target="_blank">html5boilerplate</a>.</p>

<div class="language-apache highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">ExpiresByType</span> application/javascript                "access plus 1 year"
<span class="nc">ExpiresByType</span> application/x-javascript              "access plus 1 year"
<span class="nc">ExpiresByType</span> text/javascript                       "access plus 1 year"
</code></pre></div></div>

<p>GruntJS laver et hash baseret på indholdet i filen og det hash bliver tilføjet til filnavnet.</p>

<p>Dvs. denne fil:</p>

<p><code>bundle.min.js</code></p>

<p>Kommer til at hedde:</p>

<p><code>bundle.min.e3d609e4.js</code></p>

<p>Hvis filen ændrer sig bliver der lavet en ny hash, så filnavnet ændrer sig. Dermed vil browseren se det som en ny fil, så den ikke bruger den fil den allerede har i cache, men downloade den nye fil.</p>

<p>Dermed kan jeg have filen cachet nærmest uendeligt, men stadig sikre at alle browsere får den nyeste fil, hvis jeg ændrer noget.</p>

<h3 id="article-header-id-4">Gruntfile.js</h3>

<p>For de interesserede, så er her min <code>Gruntfile.js</code> som indeholder hele den opsætning af GruntJS som er beskrevet herover. Derudover har den også compile af Sass til CSS som jeg beskriver om lidt.</p>

<pre><code class="language-javascript">module.exports = function(grunt) {
    require("load-grunt-tasks")(grunt);

    // 1. All configuration goes here
    grunt.initConfig({
        pkg: grunt.file.readJSON("package.json"),

        // grunt-contrib-concat
        concat: {
            dist: {
                src: [
                    "js/libs/prism.js",
                    "js/SlideUpBox.js",
                    "js/content-as-ecommerce.js",
                    "js/tracking.js",
                    "js/hamburgerNav.js",
                    "js/jacobworsoe.js",
                    "js/drinksberegner.js"
                ],
                dest: "js/build/bundle.js"
            }
        },

        // grunt-contrib-uglify
        uglify: {
            build: {
                files: [
                    {
                        src: "js/build/bundle.js",
                        dest: "js/build/bundle.min.js"
                    }
                ]
            }
        },

        // grunt-contrib-sass
        sass: {
            dist: {
                options: {
                    style: "compressed",
                    sourcemap: "none"
                },
                files: {
                    "css/homepage.css": "scss/homepage-bundle.sass",
                    "css/single.css": "scss/single-bundle.sass"
                }
            }
        },

        // grunt-contrib-copy
        copy: {
            main: {
                files: [
                    {
                        expand: true,
                        src: ["*.php"],
                        dest: "dist/",
                        filter: "isFile"
                    },
                    {
                        expand: true,
                        src: ["*.css"],
                        dest: "dist/",
                        filter: "isFile"
                    },
                    {
                        expand: true,
                        src: ["*.png"],
                        dest: "dist/",
                        filter: "isFile"
                    },
                    {
                        expand: true,
                        src: ["css/*.css"],
                        dest: "dist/",
                        filter: "isFile"
                    },
                    {
                        expand: true,
                        src: ["js/build/*.min.js"],
                        dest: "dist/",
                        filter: "isFile"
                    },
                    {
                        expand: true,
                        src: ["svg/*.svg"],
                        dest: "dist/",
                        filter: "isFile"
                    }
                ]
            }
        },

        // grunt-hashres
        hashres: {
            options: {
                fileNameFormat: "${name}.${hash}.${ext}",
                renameFiles: true
            },
            prod: {
                options: {},
                src: ["dist/js/**/*.min.js"],
                dest: ["dist/footer.php"]
            }
        },

        // grunt-contrib-watch
        watch: {
            options: {
                livereload: true
            },
            scripts: {
                files: ["js/*.js"],
                tasks: ["concat", "uglify"],
                options: {
                    spawn: false
                }
            },
            css: {
                files: ["scss/*.sass", "scss/*.scss"],
                tasks: ["sass"],
                options: {
                    spawn: false
                }
            }
        }

    }); // grunt.initConfig

    // 4. Where we tell Grunt what to do when we type "grunt" into the terminal.
    // Development tasks
    grunt.registerTask("default", ["sass", "watch"]);
    grunt.registerTask("stage", ["sass", "concat", "uglify", "copy"]);
    grunt.registerTask("deploy", [
        "sass",
        "concat",
        "uglify",
        "copy",
        "hashres"        
    ]);
};
</code></pre>

<h2 id="article-header-id-5">JavaScript</h2>

<h3 id="article-header-id-51">Tracking logik fra GTM til dataLayer</h3>

<p>Jeg afprøver og tester en masse forskellig tracking på mit website. Noget af det er tilføjet til sitets JavaScript fil og udstillet i <code>dataLayer</code> som det bør, men noget bliver også hurtigt tilføjet direkte i GTM for at afprøve det.</p>

<p>Jeg gik alt GTM kode igennem og fik det flyttet til sitet, så GTM indeholder så lidt kode og logik som muligt. Det er fint at teste noget hurtigt i GTM, men det skal tilføjes til <code>dataLayer</code> hvis det skal være permanent.</p>

<h3 id="article-header-id-6">Væk med jQuery</h3>

<p>Jeg har omskrevet alt jQuery til ren JavaScript for at slippe for at loade de 87 KB som jQuery fylder når det er minified (274 KB unminified). Her var <a href="http://youmightnotneedjquery.com/" rel="noopener noreferrer" target="_blank">youmightnotneedjquery.com</a> en stor hjælp.</p>

<h3 id="article-header-id-7">JavaScript reduceret fra 184 KB til 31 KB</h3>

<p>JavaScript koden til websitet er reduceret kraftigt med i alt 153 KB hvoraf de 87 KB er jQuery. Men der er også en masse andre ting jeg har skåret væk og skrevet smartere. Fx <a href="http://fitvidsjs.com/">FitVids.JS</a> som jeg brugte da jeg lavede <a href="/responsive-design-3-nemme-trin/">sitet responsivt</a> til at gøre YouTube videoer responsive. Det er meget smart, men med lidt simpel HTML og CSS kan man undvære det jQuery plugin.</p>

<p>Jeg indsætter en <code>div</code> rundt om videoen.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"videoWrapper"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;iframe</span> <span class="na">src=</span><span class="s">"//www.youtube.com/embed/usyYXNNBRjc"</span> <span class="na">frameborder=</span><span class="s">"0"</span> <span class="na">allowfullscreen</span><span class="nt">&gt;&lt;/iframe&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>

<p>Og tilføjer lidt styling af den <code>div</code> samt iframen som indeholder videoen, og så er videoen responsiv.</p>

<pre><code class="language-sass">.videoWrapper
    position: relative
    padding-bottom: 56.25%
    padding-top: 25px
    height: 0
    margin: 20px 0 20px 0
    border: 5px solid $lightGrey

.videoWrapper iframe
    position: absolute
    top: 0
    left: 0
    width: 100%
    height: 100%
</code></pre>

<p>Jeg brugte også LunaMetrics’ <a href="https://www.bounteous.com/insights/2015/05/11/youtube-tracking-google-analytics-google-tag-manager/?ns=l">script til tracking af visninger af YouTube videoer</a>, men jeg brugte ikke den tracking til noget, så det blev også fjernet.</p>

<h2 id="article-header-id-8">CSS</h2>

<h3 id="article-header-id-81">CSS skrevet i Sass</h3>

<p>I det nye redesign skrev jeg alt CSS fra bunden igen og jeg valgte at skrive det i <a href="https://sass-lang.com/" rel="noopener noreferrer" target="_blank">Sass</a>.</p>

<p>Sass er et sprog som giver nogle ekstra muligheder og Sass filerne skal compiles til almindelige CSS filer inden de ryger ud på websitet.</p>

<p>Her er mine top 3 fedeste ting ved Sass.</p>

<p><strong>1) Variabler.</strong> Jeg kan definere variabler, fx til farvekoder som er brugt mange steder i koden. Dermed kan du nemt skifte farven overalt i din kode, blot ved at ændre én variabel. Da facebook valgte at rydde op i deres CSS, fandt de 800 næsten ens blå farver i koden. Det sker ikke med Sass.</p>

<figure><a href="/assets/images/2020/08/Sass-vars-farver.jpg"><img src="/assets/images/2020/08/Sass-vars-farver.jpg" alt="Websitets farver defineret i Sass variabler." width="640" height="420" class="size-full wp-image-2372" /></a><figcaption>Websitets farver defineret i Sass variabler.</figcaption></figure>

<p><strong>2) Imports.</strong> Jeg kan splitte Sass koden op i mindre filer som tilhører en bestemt side eller sektion af sitet. Det hele kan samles til én fil, så browseren stadig kun skal lave et request.</p>

<pre><code class="language-sass">// Base
@import "normalize"
@import "_vars"
@import "_base"
@import "_jetpack"

// Critical
@import "_header"
@import "_videoEmbeds"
@import "_pre"
@import "_button"
@import "_blockquote"

// Below-the-fold
@import "_post-share-follow"
@import "_comments"
@import "_footer"
@import "_slide-up-box"

// Pages
@import "_single"
@import "_highlight"
@import "_tables"
</code></pre>

<p><strong>3) Nesting.</strong> Med Nesting kan man tilføje underliggende selectors blot ved at indent’e linjen, så man slipper for at gentage selectors mange gange.</p>

<p>Her er et eksempel på Nesting i Sass.</p>

<pre><code class="language-sass">.comment-gravatar
    float: left
    width: 15%
    max-width: 120px
    padding-right: 20px
    margin-top: 10px

    @media(max-width: 500px)
        padding-right: 8px

    img
        border-radius: 5px
</code></pre>

<p>Og her den CSS kode det compiles til.</p>

<pre><code class="language-css">.comment-gravatar {
  float: left;
  width: 15%;
  max-width: 120px;
  padding-right: 20px;
  margin-top: 10px;
}

@media(max-width: 500px) {
  .comment-gravatar {
    padding-right: 8px;
  }
}

.comment-gravatar img {
  border-radius: 5px;
}
</code></pre>

<h3 id="article-header-id-9">Inline CSS eller ekstern fil?</h3>

<p>Normalt er det best practice at have CSS i en ekstern fil, så den kan caches i browseren. Men det kræver et ekstra request at have den i en ekstern fil. Så om det kan betale sig at lave et ekstra request kommer an på hvor stor filen er samt hvor mange sider brugeren ser på sitet.</p>

<p>På første sidevisning vil det nemlig være en ulempe at have CSS i en ekstern fil, da der skal laves et request mere. Men på efterfølgende sider vil filen være cachet og skal ikke hentes igen.</p>

<figure><a href="/assets/images/2020/07/Bounce-rate-p%C3%A5-85-procent.jpg"><img src="/assets/images/2020/07/Bounce-rate-p%C3%A5-85-procent.jpg" alt="Bounce rate på 85% og 1,17 sider pr. session" width="886" height="286" class="size-full wp-image-2300" /></a><figcaption>Bounce rate på 85% og 1,17 sider pr. session</figcaption></figure>

<p>I løbet af det sidste år har der været en bounce rate på 85% på sitet, dvs. langt de fleste læser kun et enkelt blogindlæg. Der bliver også kun set 1,17 sider pr. session. Det betyder altså at 85% af de besøgende ikke ser en efterfølgende side og dermed ikke får gevinsten af en cachet CSS fil.</p>

<p>I hvert fald ikke i det samme besøg. Men det kan jo være de kommer tilbage på sitet igen og dermed stadig har CSS filen i deres cache.</p>

<figure><a href="/assets/images/2019/11/new-vs.-returning-visitors.jpg"><img src="/assets/images/2019/11/new-vs.-returning-visitors.jpg" alt="25% af de besøgende har været på sitet før." width="630" height="453" class="size-full wp-image-2094" /></a><figcaption>25% af de besøgende har været på sitet før.</figcaption></figure>

<p>Det er kun 25% af de besøgende der har været på sitet før, så langt de fleste vil ikke have CSS filen cachet.</p>

<div class="attention"><strong>Bemærk!</strong> Jeg har ekskluderet Safari her, da Safari ikke længere giver korrekte tal for tilbagevendende besøg efter ITP 2.1.</div>

<p>Min konklusion på de ovenstående data bliver at det er bedst at optimere efter at give en hurtig oplevelse på den første sidevisning og derfor lægger jeg CSS’en inline, for at spare det ekstra request.</p>

<h3 id="article-header-id-10">Inline kun den nødvendige CSS kode</h3>

<p>Når man har CSS i en ekstern fil som bliver cachet giver det typisk bedst mening at samle det hele i én fil. Men når jeg inliner min CSS kode, giver det bedre mening kun at inline den CSS kode der skal bruges på den specifikke side.</p>

<p>Min forside er rimelig simpel. I mit tilfælde er det bare en liste af mine blogindlæg med titel, dato og antal kommentarer.</p>

<figure><img src="/assets/images/2020/08/50466523-B08F-45FF-AE69-A85876863517.jpg" alt="Forsiden er meget simpel." width="400" height="437" class="size-full wp-image-2473" /><figcaption>Forsiden er meget simpel.</figcaption></figure>

<p>Et blogindlæg har både billeder og video i indlægget, den har en anderledes header med titlen på indlægget. I bunden er der links til sociale medier, tilmelding til nyhedsbrev og så er der hele kommentar sektionen, som kræver en masse CSS kode.</p>

<figure><img src="/assets/images/2020/08/716B5771-D030-4214-BF2C-4B0A8DA036D3.jpg" alt="Et blogindlæg kræver noget mere CSS." width="400" height="560" class="size-full wp-image-2474" /><figcaption>Et blogindlæg kræver noget mere CSS.</figcaption></figure>

<p>Der er altså en masse CSS kode som er helt overflødig at loade på forsiden og vice versa.</p>

<p>Når jeg bruger Sass til at samle de enkelte .sass filer til en færdig CSS fil laver jeg derfor to filer:</p>

<ol>
<li>En til forsiden og kategorisider, hvor der blot vises en liste af indlæg.</li>
<li>En til <code>single.php</code> som viser hele indlægget.</li>
</ol>

<p>De to filer har alt den generelle styling til fælles, som jeg har brudt ud i logiske moduler.</p>

<pre><code class="language-sass">// Base
@import "normalize" // https://necolas.github.io/normalize.css/
@import "_vars" // Sass variabler med alle de farver jeg bruger
@import "_base" // Site-wide styling, fx box-sizing: border-box og H1, H2, H3 og overordnet font-family
@import "_jetpack" // Jetpack tilføjer en lille statistik box, som jeg sjuler med CSS

// Critical - Above-the-fold
@import "_header" // SVG logo, sidens titel, hamburger menuen og selve menuen som åbnes

// Below-the-fold
@import "_footer" // Footer med sociale links, mit billede og en række links
</code></pre>

<p>Ovenstående CSS kode inkluderes i begge filer og derudover inkluderer jeg så den kode som er relevant for hhv. forsiden og blogindlægget.</p>

<p>Jeg har i alt 19 KB CSS kode.</p>

<p><a href="/assets/images/2019/11/Frodeling-af-bytes-i-CSS-koden.jpg"><img src="/assets/images/2019/11/Frodeling-af-bytes-i-CSS-koden-860x503.jpg" alt="" width="860" height="503" class="alignnone size-large wp-image-2099" /></a></p>

<ul>
<li>41% er Normalize, som jeg måske skal overveje om jeg kan undvære.</li>
<li>38% er specifikt til blogindlæg som jeg derved ikke behøver at loade på forsiden.</li>
<li>18% er den globale CSS til header og footer.</li>
<li>3% er til forsiden, der som sagt er meget simpel.</li>
</ul>

<p>I WordPress inkluderer jeg de to CSS filer så de ligger inline, baseret på et check for om siden er <code>single.php</code> eller andre sider.</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;style&gt;</span>
<span class="cp">&lt;?php</span> <span class="k">if</span> <span class="p">(</span> <span class="nf">is_single</span><span class="p">()</span> <span class="o">||</span> <span class="nf">is_page</span><span class="p">()</span> <span class="p">)</span> <span class="p">{</span>
    <span class="k">include</span><span class="p">(</span><span class="s2">"css/single.css"</span><span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="k">include</span><span class="p">(</span><span class="s2">"css/homepage.css"</span><span class="p">);</span>
<span class="p">}</span>
<span class="cp">?&gt;</span>
<span class="nt">&lt;/style&gt;</span>
</code></pre></div></div>

<h3 id="article-header-id-11">CSS reduceret med 45%</h3>

<p>CSS kode har det med at vokse over tid og man får sjældent ryddet op løbende. Her skrev jeg alt fra bunden og jeg er nok også blevet bedre til at skrive CSS så det fylder mindre. Resultatet er en 45% reduktion af CSS fra 34,5 KB på i det gamle design til 19 KB i det nye design.</p>

<p><a href="/assets/images/2019/11/CSS-kode-KB-jacobworsoe-v2-vs.-v3.jpg"><img src="/assets/images/2019/11/CSS-kode-KB-jacobworsoe-v2-vs.-v3.jpg" alt="" width="1002" height="601" class="alignnone size-full wp-image-2103" /></a></p>

<h3 id="article-header-id-12">Væk med !important</h3>

<p>Når jeg skriver !important i min CSS kode er det et tegn på at jeg har malet mig op i et hjørne.</p>

<p>Det er en sidste udvej. Og det kommer til at bide mig i røven senere hen.</p>

<p>Det gamle design brugte !important 35 gange.</p>

<p>Derfor har jeg fokuseret på at få gennemtænkt mine CSS selectors så jeg undgår at bruge !important i det nye design.</p>

<p>Jeg har også tænkt over at min styling skal cascade så meget som muligt, så jeg definerer mest muligt CSS kode på de øverste selectors (dem med lavest specificity) og derefter nedarves de bare til alt det øvrige. Det betyder også at jeg ikke overskriver min egen kode eller laver den samme styling flere gange på forskellige selectors.</p>

<p>Et eksempel er at jeg styler min font på <code>html</code> elementet og derefter nedarves det bare til resten af sitet, så jeg ikke skal style min font igen - lige bortset fra <code>input</code> elementer som ikke nedarver styling og derfor skal styling skrives specifikt på dem.</p>

<pre><code class="language-sass">html
    background: $grey-dark
    color: $lightGrey
    font-family: -system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"
    -webkit-font-smoothing: antialiased
    -moz-osx-font-smoothing: grayscale
    letter-spacing: 0.02em
    font-size: 20px
    line-height: 1.75
</code></pre>

<p>Tit bruges !important også for at overskrive noget andet styling, fx noget CSS der kommer med et plugin. Det betyder dermed overflødig kode, som blot overskrives.</p>

<p>Det giver også browseren mere arbejde med at finde ud af hvad der skal overskrive noget andet.</p>

<p>Jeg har, så vidt det er muligt, deaktiveret den medfølgende CSS fra de enkelte plugins, så jeg blot får den rå HTML og selv skrevet alt styling. Dermed er jeg sikker på at der ikke kommer noget overflødig CSS kode med.</p>

<h2 id="article-header-id-13">SVG til grafik</h2>

<p>SVG er super fedt til grafiske elementer fordi det er kode og ikke et billede. Dermed kan det skalere uendeligt uden at blive grimt og det fylder meget lidt.</p>

<p>Jeg bruger det fx til den lille graf i mit logo.</p>

<p>Der er forskellige måder at indsætte et SVG billede på og jeg lytter til <a href="https://css-tricks.com/">Chris Coyiers</a> enorme erfaring om SVG (han har skrevet en bog om det: <a href="https://abookapart.com/products/practical-svg" rel="noopener noreferrer" target="_blank">Practical SVG</a>). Hans anbefaling er blot at inline SVG koden direkte i HTML’en. Det har han skrevet om her: <a href="https://css-tricks.com/pretty-good-svg-icon-system/" rel="noopener noreferrer" target="_blank">A Pretty Good SVG Icon System</a></p>

<p>Jeg har alle mine SVG filer liggende i koden og selve koden til grafen i logoet kan ses herunder. Den fylder kun 848 bytes som SVG fil.</p>

<figure><a href="/assets/images/2019/10/SVG-filer.jpg"><img src="/assets/images/2019/10/SVG-filer-860x247.jpg" alt="SVG koden til grafen som fylder under 1 KB" width="860" height="247" class="size-large wp-image-2060" /></a><figcaption>SVG koden til grafen som fylder under 1 KB</figcaption></figure>

<p>Indholdet af SVG filen indsætter jeg i <code>header.php</code> med følgende kode. Når SVG filen ligger i koden, skal der ikke laves et ekstra request for at hente den og færre requests er med til at gøre sitet hurtigt.</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"/"</span> <span class="na">title=</span><span class="s">"jacobworsoe.dk"</span> <span class="na">rel=</span><span class="s">"home"</span> <span class="na">class=</span><span class="s">"blog-title"</span><span class="nt">&gt;</span>
   <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"logo"</span><span class="nt">&gt;</span><span class="cp">&lt;?php</span> <span class="k">include</span><span class="p">(</span><span class="s2">"svg/logo.svg"</span><span class="p">);</span> <span class="cp">?&gt;</span><span class="nt">&lt;/span&gt;</span>
   <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"title"</span><span class="nt">&gt;</span><span class="cp">&lt;?php</span> <span class="nf">bloginfo</span><span class="p">(</span> <span class="s1">'name'</span> <span class="p">);</span> <span class="cp">?&gt;</span><span class="nt">&lt;/span&gt;</span>
<span class="nt">&lt;/a&gt;</span>
</code></pre></div></div>

<h2 id="article-header-id-14">WordPress</h2>

<h3 id="article-header-id-141">Væk med unødvendige plugins</h3>

<p>Jeg har fået fjernet en del plugins, så det bliver mere simpelt, fjerner mulige sikkerhedshuller og gør sitet hurtigere.</p>

<h3 id="article-header-id-15">Tabeller</h3>

<p><a href="https://wordpress.org/plugins/tablepress/" rel="noopener noreferrer" target="_blank">TablePress</a> er et fedt plugin, men de få simple tabeller jeg har i mine indlæg, kan jeg sagtens skrive i hånden, så væk med det.</p>

<h3 id="article-header-id-16">Syntax highlighting</h3>

<p>Jeg brugte <a href="https://github.com/googlearchive/code-prettify">Code Prettify</a> til syntax highlighting af kode. Jeg er skiftet til at bruge <a href="https://prismjs.com/" rel="noopener noreferrer" target="_blank">Prism.js</a> hvor jeg vælger præcis de kodesprog jeg skal bruge og så får jeg en CSS fil og en JavaScript fil. CSS filen inkluderer jeg i min SCSS fil og JavaScript filen bliver bundlet sammen med min øvrige JS kode i én samlet fil. Og så er dét plugin overflødigt :)</p>

<p>Jeg kan i øvrigt anbefale <a href="https://css-tricks.com/posting-code-blocks-wordpress-site/" rel="noopener noreferrer" target="_blank">denne artikel</a> med alle de forskellige muligheder for at skrive og vise kode i WordPress.</p>

<h3 id="article-header-id-17">Relaterede indlæg</h3>

<p>Jeg har brugt Yet Another Related Posts Plugin til at vise relaterede indlæg i bunden af hvert blogindlæg.</p>

<figure><a href="/assets/images/2019/08/Relaterede-blogindl%C3%A6g.png"><img src="/assets/images/2019/08/Relaterede-blogindl%C3%A6g.png" alt="I bunden af alle blogindlæg vises links til relaterede blogindlæg." width="941" height="595" class="size-full wp-image-1829" /></a><figcaption>I bunden af alle blogindlæg vises links til relaterede blogindlæg.</figcaption></figure>

<p>Jeg brugte Enhanced Ecommerce til at tracke impressions og clicks på dem og fandt ud af at de links havde en CTR på 0,42% så for 99,58% var de bare ligegyldigt støj på siden. Så jeg fjernede dem inklusiv det plugin.</p>

<figure><a href="/assets/images/2019/08/Product-list-performance-nyeste-og-relaterede-indl%C3%A6g.png"><img src="/assets/images/2019/08/Product-list-performance-nyeste-og-relaterede-indl%C3%A6g.png" alt="CTR på 0,42% viser at meget få klikker på de links." width="1094" height="485" class="size-full wp-image-1830" /></a><figcaption>CTR på 0,42% viser at meget få klikker på de links.</figcaption></figure>

<p>Jeg fjernede i øvrigt også links til de seneste blogindlæg, da de havde en endnu lavere CTR på 0,05% - disse blev ikke lavet med et plugin, men det er altid godt at få fjernet unødvendigt støj.</p>

<figure><a href="/assets/images/2019/08/Nyeste-blogindl%C3%A6g-imressions-clicks-CTR.png"><img src="/assets/images/2019/08/Nyeste-blogindl%C3%A6g-imressions-clicks-CTR.png" alt="Sidebar med nyeste blogindlæg." width="1270" height="526" class="size-full wp-image-1828" /></a><figcaption>Sidebar med nyeste blogindlæg.</figcaption></figure>

<h2 id="article-header-id-18">Loadtid og konvertering</h2>

<p>Okay, det var en lang teknisk snak. Nu skal vi se om det har givet de ønskede resultater.</p>

<h3 id="article-header-id-181">Google Pagespeed Score hævet fra 86 til 99</h3>

<p>Det gamle design havde en pagespeed score på 86 for desktop. <a href="https://developers.google.com/speed/pagespeed/insights/?url=https://www.jacobworsoe.dk/&amp;tab=desktop" rel="noopener noreferrer" target="_blank">Den er nu 99</a>.</p>

<figure><a href="/assets/images/2020/08/pagespeed_score_99_desktop.jpg"><img src="/assets/images/2020/08/pagespeed_score_99_desktop.jpg" alt="Pagespeed score på 99 for desktop." width="701" height="467" class="size-full wp-image-2337" /></a><figcaption>Pagespeed score på 99 for desktop.</figcaption></figure>

<p>Den vigtige metric er dog mobile nu og <a href="https://developers.google.com/speed/pagespeed/insights/?url=https://www.jacobworsoe.dk/&amp;tab=mobile" rel="noopener noreferrer" target="_blank">den er 96</a>.</p>

<figure><a href="/assets/images/2020/08/pagespeed_score_96_mobile.jpg"><img src="/assets/images/2020/08/pagespeed_score_96_mobile.jpg" alt="Pagespeed score på 96 for mobile." width="687" height="477" class="size-full wp-image-2338" /></a><figcaption>Pagespeed score på 96 for mobile.</figcaption></figure>

<h3 id="article-header-id-19">Er sitet så blevet hurtigere?</h3>

<p>Ja, det er det. I gennemsnit er loadtiden blevet forbedret 29%.</p>

<figure><a href="/assets/images/2020/08/Average-pageload-times.jpg"><img src="/assets/images/2020/08/Average-pageload-times.jpg" alt="Den gennemsnitlige loadtid er forbedret 29%." width="592" height="327" class="size-full wp-image-2367" /></a><figcaption>Den gennemsnitlige loadtid er forbedret 29%.</figcaption></figure>

<p>Men gennemsnit kan snyde meget og skjule sandheden.</p>

<p>Ikke alle sider loader lige hurtigt.</p>

<p>Jeg har blogindlæg på mere end <a href="/indhold-enhanced-ecommerce/">6000 ord med masser af billeder</a>. Jeg har meget populære blogindlæg som står for <a href="/hvor-meget-drikker-gaesterne-til-et-bryllup/">70% af sidevisningerne</a> som kun har få, men til gengæld meget store billeder. Og så er der <a href="/">forsiden</a> som stort set kun er tekst.</p>

<p>Især det faktum at de har meget forskellige antal sidevisninger gør at de fylder meget forskelligt i gennemsnittet.</p>

<p>Lad os derfor kigge på top 10 mest populære sider hver for sig, samt et vægtet gennemsnit for de sider. Alle top 10 sider er blevet hurtigere, men der er stor forskel på hvor meget de er forbedret.</p>

<figure><a href="/assets/images/2020/08/Udvikling-i-loadtid-top-10-sider.jpg"><img src="/assets/images/2020/08/Udvikling-i-loadtid-top-10-sider-860x496.jpg" alt="Top 10 sider er i gennemsnit blevet 22% hurtigere - men der er store forskelle!" width="860" height="496" class="size-large wp-image-2368" /></a><figcaption>Top 10 sider er i gennemsnit blevet 22% hurtigere - men der er store forskelle!</figcaption></figure>

<p>22% er et mere retvisende gennemsnit for udviklingen i loadtid.</p>

<h3 id="article-header-id-20">Hvad med konvertering?</h3>

<p>Jeg har tidligere skrevet om hvordan jeg bruger Enhanced Ecommerce til at <a href="/indhold-enhanced-ecommerce/">tracke om brugerne læser mine blogindlæg</a>.</p>

<p>Kort fortalt tracker jeg hvor mange der scroller helt til bunden af et blogindlæg og har været mindst 1 minut på siden. Det er den vigtigste KPI for min blog. Hvor mange læser hele blogindlægget?</p>

<p>For hvert blogindlæg har jeg en Buy-to-detail Rate, som er forholdet mellem antal sidevisninger og antal læsninger.</p>

<p>På trods af at loadtiden er markant forbedret for alle blogindlæg, så er konverteringen desværre ikke steget - tværtimod.</p>

<p>Jeg tror den store årsag til den lavere konvertering skyldes designet. Jeg har skruet op for <code>font-size</code> fra <code>17px</code> til <code>20px</code> i det nye design og gjort overskrifter markant større og givet det hele lidt mere “luft”. Det gør det nemmere at læse, men siden bliver også markant længere. Måske føles det som et længere blogindlæg at tygge sig igennem?</p>

<figure><a href="/assets/images/2020/08/Udvikling-i-konvertering-top-10-sider.jpg"><img src="/assets/images/2020/08/Udvikling-i-konvertering-top-10-sider.jpg" alt="Udvikling i konvertering - top 10 sider" width="1058" height="645" class="size-full wp-image-2377" /></a><figcaption>Udvikling i konvertering - top 10 sider</figcaption></figure>

<p>Bounce Rate er ligeledes uændret, på trods af den hurtigere loadtid.</p>

<figure><a href="/assets/images/2020/08/Bounce-Rate-comparison.jpg"><img src="/assets/images/2020/08/Bounce-Rate-comparison.jpg" alt="Bounce Rate før/efter designet." width="915" height="497" class="size-full wp-image-2378" /></a><figcaption>Bounce Rate før/efter designet.</figcaption></figure>

<p>Så det nye design har ikke haft den ønskede effekt på konverteringen. Det må jeg gøre bedre i næste design.</p>

<h3 id="article-header-id-21">Øge sidevisninger pr. besøg</h3>

<p>Jeg har læst <a href="https://www.saxo.com/dk/dont-make-me-think-revisited_steve-krug_paperback_9780321965516">Don’t Make Me Think</a> mange gange og den kan anbefales til alle der arbejder med noget digitalt.</p>

<figure><a href="/assets/images/2020/08/Dont_make_me_think.jpg"><img src="/assets/images/2020/08/Dont_make_me_think.jpg" alt="Don&#039;t Make Me Think af Steve Krug" width="622" height="622" class="size-full wp-image-2383" /></a><figcaption>Don't Make Me Think af Steve Krug</figcaption></figure>

<p>Her er en god pointe fra bogen omkring navigation.</p>

<figure><a href="/assets/images/2020/08/The-overlooked-purpose-of-navigation.jpg"><img src="/assets/images/2020/08/The-overlooked-purpose-of-navigation-860x486.jpg" alt="Navigation reveals content!" width="860" height="486" class="size-large wp-image-2384" /></a><figcaption>Navigation reveals content!</figcaption></figure>

<p>Som tidligere vist, så har sitet en Bounce Rate på 85% og der bliver kun set 1,17 sider pr. session.</p>

<figure><a href="/assets/images/2020/07/Bounce-rate-p%C3%A5-85-procent.jpg"><img src="/assets/images/2020/07/Bounce-rate-p%C3%A5-85-procent.jpg" alt="Bounce rate på 85% og 1,17 sider pr. session" width="886" height="286" class="size-full wp-image-2300" /></a><figcaption>Bounce rate på 85% og 1,17 sider pr. session</figcaption></figure>

<p>Jeg vil gerne at brugerne fortsætter rundt på sitet og ser nogle flere blogindlæg.</p>

<p>Det gamle design havde ikke en menu, så jeg tilføjede en burger menu som viser sitets kategorier, som Steve Krug anbefaler i Don’t Make Me Think.</p>

<figure><a href="/assets/images/2020/08/burger-menu-open.jpg"><img src="/assets/images/2020/08/burger-menu-open.jpg" alt="Burger menu med kategorier." width="798" height="552" class="size-full wp-image-2386" /></a><figcaption>Burger menu med kategorier.</figcaption></figure>

<p>Jeg tracker både hvor mange der åbner burger menuen og hvor mange der klikker i den.</p>

<ul>
    <li>1,6% af alle besøg åbner menuen.</li>
    <li>0,4% af alle besøg klikker på noget i menuen.</li>
</ul>

<p>Her er de kategorier der bliver klikket på.</p>

<figure><a href="/assets/images/2020/08/Click-in-hamburger-navigation.jpg"><img src="/assets/images/2020/08/Click-in-hamburger-navigation.jpg" alt="Mest klikkede kategorier i burger menuen." width="762" height="587" class="size-full wp-image-2385" /></a><figcaption>Mest klikkede kategorier i burger menuen.</figcaption></figure>

<p>Umiddelbart en ret lav konverteringsrate og sider pr. besøg er dog også uændret.</p>

<figure><a href="/assets/images/2020/08/Pages-per-session.jpg"><img src="/assets/images/2020/08/Pages-per-session.jpg" alt="Pages per session er uændret." width="463" height="266" class="size-full wp-image-2388" /></a><figcaption>Pages per session er uændret.</figcaption></figure>

<p>Så selvom Steve Krug har ret i mange ting, så virker en burger menu altså ikke på dette site. Jeg må i tænkeboks.</p>

<p>Til sammenligning er der 0,47% der klikker på et internt link når jeg i et blogindlæg, linker til et andet af mine blogindlæg.</p>

<h2 id="article-header-id-22">Lykkedes målene?</h2>

<p>Lad os se.</p>

<ol>
<li>Mere moderne workflow til udvikling af websitet - <span class="task-status-completed">Tjek!</span></li>
<li>Oprydning i kode og sletning af unødvendige ting - <span class="task-status-completed">Tjek!</span></li>
<li>Hurtigere loadtid - <span class="task-status-completed">Tjek!</span></li>
<li>Højere konvertering - <span class="task-status-failed">Nope!</span></li>
<li>Øge sidevisninger pr. besøg - <span class="task-status-failed">Nope!</span></li>
</ol>

<style>
.task-status-completed { color: #48e0a4 }
.task-status-failed { color: #ffafaf }
</style>]]></content><author><name></name></author><category term="Webdesign" /><summary type="html"><![CDATA[Jeg startede denne blog i 2009 og den første version var bygget helt fra bunden hvor jeg havde kodet det hele i PHP med MySQL som database.]]></summary></entry><entry><title type="html">Tracking af indhold med Enhanced Ecommerce</title><link href="https://www.jacobworsoe.dk/indhold-enhanced-ecommerce/" rel="alternate" type="text/html" title="Tracking af indhold med Enhanced Ecommerce" /><published>2020-07-31T23:33:15+00:00</published><updated>2020-07-31T23:33:15+00:00</updated><id>https://www.jacobworsoe.dk/indhold-enhanced-ecommerce</id><content type="html" xml:base="https://www.jacobworsoe.dk/indhold-enhanced-ecommerce/"><![CDATA[<p>Hvilke KPI’er er vigtige for en blog?</p>

<p>Antal månedlige besøg er vigtig.</p>

<p>Trafik fordelt på kanaler er også vigtig at vide. Og måske også om de besøgende tilmelder sig nyhedsbrevet.</p>

<h3>Men hvordan måles konvertering?</h3>

<p>Man kan tracke når nogen skriver en kommentar. Men da diskussionen i høj grad er flyttet over til de sociale medier hvor indlægget deles, så er det ikke et godt mål for konvertering på en blog. Siden jeg startede min blog i 2009 har jeg til dato haft 118.121 besøg og 482 kommentarer, hvilket giver en konverteringsrate på 0,4%.</p>

<p>Det kan også være tilmelding til RSS feed, men selvom det er min foretrukne måde at holde mig opdateret med en lang række blogs, så har jeg på fornemmelsen, at det er meget få der bruger det. Det samme med nyhedsbreve for blogs.</p>

<p>Endelig er der metrics som bounce rate og time on page. Men de er svære at konkludere noget ud fra isoleret set. Jeg har en bounce rate på 85% på dette site. Det er højt, men det betyder ikke nødvendigvis at mine blogindlæg ikke bliver læst grundigt. Det kan sagtens være brugerne er på sitet mange minutter og stadig bouncer. Det betyder bare at de kun læser ét blogindlæg.</p>

<figure><a href="/assets/images/2020/07/Bounce-rate-p%C3%A5-85-procent.jpg"><img src="/assets/images/2020/07/Bounce-rate-p%C3%A5-85-procent.jpg" alt="85% bounce rate." width="886" height="286" class="size-full wp-image-2300" /></a><figcaption>85% bounce rate.</figcaption></figure>

<p>Time on page bliver ikke målt på den sidste side i et besøg, så med en bounce rate på 85% er det sjældent den bliver målt. Den metric er også farlig at konkludere noget ud fra, da brugeren ikke nødvendigvis forlader siden, men fx blot loader en ny side i en anden tab.</p>

<h2 id="article-header-id-0">Bliver indholdet rent faktisk læst?</h2>

<p>Selvom det er fedt med mange besøg og sidevisninger, så siger det ikke i sig selv noget om kvaliteten af indholdet. Det er vigtigt at vide om brugerne rent faktisk læser indlægget.</p>

<p>Og det er lige præcis den form for konvertering der er vigtig for en blog, så i dette indlæg viser jeg hvordan jeg bruger Google Analytics Enhanced Ecommerce til at tracke mit indhold og den verden af nye indsigter det åbner op for.</p>

<h2 id="article-header-id-1">Inspireret af Simo Ahava</h2>

<p>Jeg elsker at finde nye kreative måder at bruge Google Analytics og Enhanced Ecommerce til at indsamle data og få et nyt indblik i alt fra websites til den fysiske verden.</p>

<p>Det gør Simo Ahava også og mit tracking setup er også kraftigt inspireret af det setup han har lavet på sin egen blog og skrevet om her: <a href="https://www.simoahava.com/analytics/track-content-enhanced-ecommerce/" rel="noopener noreferrer">Track Content With Enhanced Ecommerce</a></p>

<p>Selve logikken i koden er kopieret fra Simo’s <a href="https://github.com/sahava/eec-gtm" rel="noopener noreferrer">Github</a> som er baseret på <a href="http://cutroni.com/blog/2014/02/12/advanced-content-tracking-with-universal-analytics/" rel="noopener noreferrer">Justin Cutroni’s scroll script</a>.</p>

<p>Jeg har derefter omskrevet det til ren JavaScript, så det ikke er afhængigt af jQuery, for at slippe for at skulle loade jQuery på sitet. Jeg brugte den her rigtig meget for at konvertere koden: <a href="http://youmightnotneedjquery.com" rel="noopener noreferrer">youmightnotneedjquery.com</a></p>

<p>Derudover har jeg lavet tre udvidelser af koden:</p>

<ul>
<li>Jeg tracker kun impressions af blogindlæg på forsiden og andre lister, hvis de har været synlige på skærmen</li>
<li>Jeg tracker brugere der kun skimmer artiklen, uden at læse den</li>
<li>Alle data er udstillet i <code>dataLayer</code> og jeg scraper derfor ikke noget indhold med JavaScript</li>
</ul>

<p>Mere om det senere.</p>

<h2>Tracking af læsning af indhold med Enhanced Ecommerce</h2>

<p>Okay, lad os lige starte med at se på hvordan man overhovedet kan bruge Enhanced Ecommerce til at tracke når brugerne læser indholdet på en blog. Det kræver nemlig at man er lidt kreativ med fortolkningerne af Ecommerce og checkout.</p>

<p>Her er definitionerne af Enhanced Ecommerce, som jeg bruger på min blog:</p>

<ul>
<li><strong>Produkt:</strong> Et blogindlæg.</li>
<li><strong>Produkt navn:</strong> Titlen på blogindlægget.</li>
<li><strong>Produkt pris:</strong> Antal ord i blogindlægget.</li>
<li><strong>Produkt kategori:</strong> WordPress kategori, fx "Webanalyse" eller "SEO".</li>
<li><strong>Produkt brand:</strong> Blogindlæggets udgivelsesår.</li>
<li><strong>Produkt impression:</strong> Når et blogindlæg bliver vist på en liste, fx forsiden, kategorisider, tagsider, relateret blogindlæg eller lignende.</li>
<li><strong>Produkt lister:</strong> Alle ovenstående lister.</li>
<li><strong>Produkt click:</strong> Når en bruger klikker på et blogindlæg på en af ovenstående lister.</li>
<li><strong>Produkt detail view:</strong> Når et blogindlæg vises (dvs. sidevisning af et blogindlæg).</li>
<li><strong>Produkt add to cart:</strong> Når brugeren begynder at scrolle og dermed om de er begyndt at læse indlægget.</li>
<li><strong>Produkt checkout:</strong> Der er 3 steps i checkout, som er afhængige af hvor langt brugeren scroller. Step 1 er 33%, step 2 er 66% og step 3 er 100% af indlægget. Her måler jeg kun højden på selve indlægget, dvs. kommentarer er ikke med.</li>
<li><strong>Gennemført købt:</strong> Når brugeren har scrollet 100% af indlægget og været mindst 1 minut på siden. Dermed kan jeg antage at indlægget rent faktisk er læst og ikke bare skimmet.</li>
</ul>

<h2>Lad os få styr på produktdata</h2>

<p>På alle sider hvor der vises et eller flere indlæg, skal der trackes en række produktdata for hvert indlæg.</p>

<p>ID og navnet (titlen) på indlægget kan fanges i Wordpress i <a href="https://codex.wordpress.org/The_Loop" rel="noopener noreferrer" target="_blank">The Loop</a> med de indbyggede funktioner <code>the_title()</code> og <code>the_ID()</code> og gemmes i en JavaScript variabel, så de kan sendes til Google Analytics. Årstallet som gemmes i brand hentes med <code>the_time('Y')</code>.</p>

<p>Et indlæg kan tilhøre flere kategorier og hvis du bruger <a href="https://wordpress.org/plugins/wordpress-seo/" rel="noopener noreferrer" target="_blank">Yoast SEO</a> er der mulighed for at angive en primær kategori. Hvis et indlæg tilhører flere kategorier, bruges kun den primære. Den logik kan kodes således:</p>

<pre><code class="language-php">// SHOW YOAST PRIMARY CATEGORY, OR FIRST CATEGORY
$category = get_the_category();
$useCatLink = true;

// If post has a category assigned.
if ($category){
    $category_display = '';
    $category_link = '';
    if ( class_exists('WPSEO_Primary_Term') )
    {
        // Show the post's 'Primary' category, if this Yoast feature is available, &amp; one is set
        $wpseo_primary_term = new WPSEO_Primary_Term( 'category', get_the_id() );
        $wpseo_primary_term = $wpseo_primary_term-&gt;get_primary_term();
        $term = get_term( $wpseo_primary_term );
        if (is_wp_error($term)) { 
            // Default to first category (not Yoast) if an error is returned
            $category_display = $category[0]-&gt;name;
            $category_link = get_category_link( $category[0]-&gt;term_id );
        } else { 
            // Yoast Primary category
            $category_display = $term-&gt;name;
            $category_link = get_category_link( $term-&gt;term_id );
        }
    } 
    else {
        // Default, display the first category in WP's list of assigned categories
        $category_display = $category[0]-&gt;name;
        $category_link = get_category_link( $category[0]-&gt;term_id );
    }

    // Display category
    if ( empty($category_display) ) {
        $category_display = "Ingen kategori";
    }   
}
</code></pre>

<p>Derefter udskrives kategorien med <code>&lt;?php echo $category_display; ?&gt;</code>.</p>

<p>Prisen er antal ord og der har PHP en indbygget funktion <code>str_word_count</code> som tæller antal ord i en streng - i dette tilfælde den aktuelle artikel. Jeg har lagt det i en funktion i <code>functions.php</code> så jeg nemt kan kalde den med word_count() i <code>single.php</code>.</p>

<pre><code class="language-php">function word_count() {
    $content = get_post_field( 'post_content', $post-&gt;ID );
    $word_count = str_word_count( strip_tags( $content ) );
    return $word_count;
}
</code></pre>

<p>I Enhanced Ecommerce er det vigtigt at produktdata er formateret med præcis den rigtige syntaks - ellers vil der ikke blive sendt nogen produktdata til Google Analytics for dét request. Det færdige <code>Product</code> object med den korrekte syntaks ser således ud:</p>

<pre><code class="language-javascript">var product = [{
    name: '<?php the_title(); ?>',
    id: '<?php the_ID(); ?>',
    price: '<?php echo word_count(); ?>',
    brand: '<?php the_time('Y') ?>',
    category: '<?php echo $category_display; ?>',
    variant: '',
    quantity: 1
  }];
</code></pre>

<h2>Product impressions efter 2 sekunder</h2>

<p>Det første trin i kunderejsen er product impressions på en produktliste.</p>

<p>Der vises lister af blogindlæg på forsiden, kategorisider, som relaterede indlæg, etc. Der er primært to ting der er vigtige når der trackes impressions.</p>

<ol>
<li><p>Der skal kun trackes impressions af blogindlæg som brugeren rent faktisk har set. Det er ikke nok at linket til  blogindlægget har været længere nede på siden (below the fold), eller at brugeren har scrollet lynhurtigt forbi det. Brugeren skal have set blogindlægget og jeg skal være rimelig sikker på at brugeren har set og forholdt sig til det.</p></li>
<li><p>Trackingen må ikke sløve brugerens computer og gøre sitet langsomt. Når jeg skal holde øje med hvor langt brugeren scroller ned af siden og dermed om et givent blogindlæg er blevet synligt på skærmen, kan jeg nemt risikere at der skal køres noget JavaScript kode meget ofte, især hvis brugeren scroller hurtigt ned over siden. Dette kan påvirke hvor gnidningsfrit scrollet opleves for brugeren.</p></li>
</ol>

<p>I værste fald kan det gøre sitet ubrugeligt, som det <a href="https://johnresig.com/blog/learning-from-twitter/" rel="noopener noreferrer" target="_blank">skete for Twitter tilbage i 2011</a>.</p>

<blockquote><p>Depending upon the browser the scroll event can fire a lot and putting code in the scroll callback will slow down any attempts to scroll the page (not a good idea). Instead it’s much better to use some form of a timer to check every X milliseconds OR to attach a scroll event and only run your code after a delay.<cite><a href="https://johnresig.com/blog/learning-from-twitter/" target="_blank" rel="noopener noreferrer">John Resig, skaberen af jQuery</a></cite></p></blockquote>

<p><strong>Begge ting kan løses med en debounce funktion.</strong></p>

<p>En debounce funktion siger: “Udfør denne kode når noget ikke er sket i X antal millisekunder”.</p>

<p><a href="https://css-tricks.com/debouncing-throttling-explained-examples/" rel="noopener noreferrer" target="_blank">Denne artikel fra CSS-Tricks.com</a> har nogle gode visualiseringer og demoer som viser hvordan debounce virker. David Walsh har også <a href="https://davidwalsh.name/javascript-debounce-function" rel="noopener noreferrer" target="_blank">skrevet om det her</a>.</p>

<figure><a href="/assets/images/2020/07/css-tricks-debounce.png"><img src="/assets/images/2020/07/css-tricks-debounce.png" alt="Kilde: CSS-Tricks.com" width="661" height="133" class="size-full wp-image-2328" /></a><figcaption>Kilde: <a href="https://css-tricks.com/debouncing-throttling-explained-examples/">CSS-Tricks.com</a></figcaption></figure>

<p>I dette tilfælde køres koden når brugeren stopper med at scrolle i 2 sekunder. Hvis brugeren scroller igen inden de 2 sekunder er gået, nulstilles timeren og når brugeren stopper med at scrolle, starter timeren igen fra 0 og hvis der går 2 sekunder uden scroll, udføres koden.</p>

<p>Og sidst men ikke mindst skal der kun trackes impressions for et produkt én gang på hver side.</p>

<p>Lad os kigge på koden.</p>

<p>Først er der lavet tre funktioner. Den første tjekker om et element er synligt på skærmen.</p>

<pre><code class="language-javascript">function checkVisible(elm) {
  var rect = elm.getBoundingClientRect();
  var viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight);
  return !(rect.bottom &lt; 0 || rect.top - viewHeight &gt;= 0);
}   
</code></pre>

<p class="attention"><strong>Bemærk!</strong> Jeg lavede dette tilbage i 2016. Hvis det skal laves i dag, kan du med fordel bruge <a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API">Intersection Observer</a> som er et asynkront API til netop at holde øje med hvornår elementer bliver synlige i viewport og den kan også holde styr på hvilke der har været synlige, så man ikke selv skal sørge for at de ikke tracker flere gange. <a href="https://caniuse.com/#feat=intersectionobserver">Browser support</a> er rigtig god i dag, så den bør bruges fremover.
</p>

<p>Den næste laver et Enhanced Ecommerce product object med de relevante produktdata for et blogindlæg som er synligt på skærmen.</p>

<pre><code class="language-javascript">function pushProducts(productElement, i) {
  ga_products.push({
    name: productElement[i].dataset.title,
    id: productElement[i].dataset.id,
    price: productElement[i].dataset.price,
    brand: productElement[i].dataset.year,
    category: productElement[i].dataset.category,
    variant: productElement[i].dataset.author,
    list: pageType,
    position: productElement[i].dataset.position
  });
}
</code></pre>

<p>Produktdataene er placeret i data attributter i HTML koden.</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;h1</span> <span class="na">class=</span><span class="s">"home-post-headline"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;a</span> <span class="na">href=</span><span class="s">"/returvarer-google-analytics/"</span> 
        <span class="na">data-title=</span><span class="s">"Tracking af returvarer i Google Analytics (den ultimative guide)"</span> 
        <span class="na">data-id=</span><span class="s">"1597"</span> 
        <span class="na">data-category=</span><span class="s">"Webanalyse"</span> 
        <span class="na">data-year=</span><span class="s">"2019"</span> 
        <span class="na">data-author=</span><span class="s">"2"</span> 
        <span class="na">data-price=</span><span class="s">"3668"</span> 
        <span class="na">data-position=</span><span class="s">"2"</span>
        <span class="na">class=</span><span class="s">"home-post-link"</span><span class="nt">&gt;</span>
        Tracking af returvarer i Google Analytics (den ultimative guide)            
    <span class="nt">&lt;/a&gt;</span>
<span class="nt">&lt;/h1&gt;</span>
</code></pre></div></div>

<p>Den tredje funktion sender impressions til Google Analytics.</p>

<pre><code class="language-javascript">function sendProducts(trigger) {
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    event: trigger,
    ecommerce: {
      impressions: window.ga_products
    }
  });
  window.ga_products = [];
}</code></pre>

<p>Ved pageload bygges et JavaScript <code>object</code> med sidens blogindlæg og der checkes om nogle af sidens blogindlæg er synlige, dvs. dem som er above-the-fold. De synlige blogindlæg pushes til products objectet med <code>pushProducts()</code>.</p>

<p>Hvis der er blogindlæg som ikke er synlige, tilføjes de til <code>ga_products_not_visible</code> objectet som vi derefter kan holde øje med om de bliver synlige i 2 sekunder og tracke en impression for dem.</p>

<pre><code class="language-javascript">// Cache product element
var articles = document.querySelectorAll(".home-post-headline a");

// See if products are in view
if (articles &amp;&amp; articles.length &gt; 0) {
  for (var i = 0; i &lt; articles.length; i++) {
    if (checkVisible(articles[i])) {
      pushProducts(articles, i);
    } else {
      ga_products_not_visible.push(articles[i]);
    }
  }
}</code></pre>

<p>Hvis der var nogle synlige blogindlæg, pushes de til dataLayer, så de bliver sendt med, sammen med pageview requestet for siden.</p>

<pre><code class="language-javascript">// If any products was in view on pageload, send those products
if (ga_products.length &gt; 0) {
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    ecommerce: {
      impressions: window.ga_products
    }
  });
  window.ga_products = [];
}</code></pre>

<p>Okay, nu har vi styr på blogindlæg som er above-the-fold. Nu skal vi tracke impressions for dem som er below-the-fold.</p>

<p>Fordi det kan være lidt tungt at tracke på scroll, så skal det kun gøres hvis der rent faktisk er nogle produkter below-the-fold, så det starter vi med at tjekke med <code>ga_products_not_visible.length</code>.</p>

<p>Derefter defineres en funktion som køres hver gang der scrolles.</p>

<p>Det første funktionen gør, når der scrolles er <code>clearTimeout</code> som nulstiller timeren.</p>

<p>Derefter startes en ny <code>setTimeout</code> som indeholder den kode som køres efter 2000 millisekunder.</p>

<p>Når de 2000 millisekunder er gået udføres koden, som tjekker om nogle af de blogindlæg der endnu ikke er tracket en impression af, er synlige på skærmen. Hvis det er tilfældet bliver de tilføjet til products objectet og fjernet fra listen over blogindlæg der ikke er tracker en impression for, så de ikke kan trackes igen. Til sidst sendes de til Google Analytics med et event.</p>

<p><strong>Men!</strong> Hvis der scrolles inden de 2000 millisekunder er gået, så køres hele koden igen og det første i koden er at nulstille timeren og starte den på ny som beskrevet herover.</p>

<p>Dette er “magien” bag en debounce funktion. Den sørger for at koden ikke afvikles med det samme der scrolles, men først efter en pause på 2000 millisekunder, hvilket både gør sitet mere flydende, men også sikrer at brugeren skal stoppe med at scrolle i 2000 millisekunder (og dermed formentlig har vurderet om de vil klikke på linket) før vi tracker en impression.</p>

<pre><code class="language-javascript">// If page contained products not in view, start the scroll tracker
if (ga_products_not_visible.length &gt; 0) {
  var scrollTimeout;

  function checkProductsInViewOnScroll() {
    clearTimeout(scrollTimeout);

    scrollTimeout = setTimeout(function() {
      for (var i = ga_products_not_visible.length - 1; i &gt;= 0; i--) {
        if (checkVisible(ga_products_not_visible[i])) {
          pushProducts(ga_products_not_visible, i);

          // Remove the product in view from ga_products_not_visible
          ga_products_not_visible.splice(i, 1);
        }
      }

      if (ga_products.length &gt; 0) {
        sendProducts("moreImpressionsSent");
      }
    }, 2000);
  }

  // Start scroll listener
  window.addEventListener("scroll", checkProductsInViewOnScroll);
}</code></pre>

<p>Her er den samlede kode til at tracke impressions på lister.</p>

<pre><code class="language-javascript">// Set objects to store posts
window.ga_products = window.ga_products || [];
window.ga_products_not_visible = window.ga_products_not_visible || [];

function checkVisible(elm) {
  var rect = elm.getBoundingClientRect();
  var viewHeight = Math.max(
    document.documentElement.clientHeight,
    window.innerHeight
  );
  return !(rect.bottom &lt; 0 || rect.top - viewHeight &gt;= 0);
}

function pushProducts(productElement, i) {
  ga_products.push({
    name: productElement[i].dataset.title,
    id: productElement[i].dataset.id,
    price: productElement[i].dataset.price,
    brand: productElement[i].dataset.year,
    category: productElement[i].dataset.category,
    variant: productElement[i].dataset.author,
    list: pageType,
    position: productElement[i].dataset.position
  });
}

function sendProducts(trigger) {
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    event: trigger,
    ecommerce: {
      impressions: window.ga_products
    }
  });
  window.ga_products = [];
}

// Cache product element
var articles = document.querySelectorAll(".home-post-headline a");

// See if products are in view
if (articles &amp;&amp; articles.length &gt; 0) {
  for (var i = 0; i &lt; articles.length; i++) {
    if (checkVisible(articles[i])) {
      pushProducts(articles, i);
    } else {
      ga_products_not_visible.push(articles[i]);
    }
  }
}

// If any products was in view on pageload, send those products
if (ga_products.length &gt; 0) {
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    ecommerce: {
      impressions: window.ga_products
    }
  });
  window.ga_products = [];
}

// If page contained products not in view, start the scroll tracker
if (ga_products_not_visible.length &gt; 0) {
  var scrollTimeout;

  function checkProductsInViewOnScroll() {
    clearTimeout(scrollTimeout);

    scrollTimeout = setTimeout(function() {
      for (var i = ga_products_not_visible.length - 1; i &gt;= 0; i--) {
        if (checkVisible(ga_products_not_visible[i])) {
          pushProducts(ga_products_not_visible, i);

          // Remove the product in view from ga_products_not_visible
          ga_products_not_visible.splice(i, 1);
        }
      }

      if (ga_products.length &gt; 0) {
        sendProducts("moreImpressionsSent");
      }
    }, 2000);
  }

  // Start scroll listener
  window.addEventListener("scroll", checkProductsInViewOnScroll);
}
</code></pre>

<p>Antal impressions falder dermed jo længere ned på forsiden man kommer og de første to positioner har stort set samme antal impressions, da de er above-the-fold, både på mobile og desktop.</p>

<figure><a href="/assets/images/2020/07/Impressions-fordelt-p%C3%A5-positioner-p%C3%A5-forsiden.png"><img src="/assets/images/2020/07/Impressions-fordelt-p%C3%A5-positioner-p%C3%A5-forsiden-860x477.png" alt="Impressions fordelt på positioner på forsiden." width="860" height="477" class="size-large wp-image-2315" /></a><figcaption>Impressions fordelt på positioner på forsiden.</figcaption></figure>

<h2>Produkt click med et callback</h2>

<p>Når brugeren klikker på et blogindlæg, skal klikket trackes. Udfordringen er at siden skifter når man klikker og derfor er der risiko for at requestet ikke når at blive sendt til Google Analytics, før siden er skiftet og dermed bliver det aldrig sendt.</p>

<p>Løsningen er at annullere sideskiftet med JavaScript og istedet få GTM til at <a href="https://www.simoahava.com/gtm-tips/hitcallback-eventcallback/" rel="noopener noreferrer" target="_blank">lave sideskiftet i et callback</a> efter requestet er succesfuldt sendt til Google Analytics.</p>

<p>Der er særligt to ting der er vigtige at tage højde for når man annullerer et sideskift.</p>

<ol>
<li><p>Hvis requestet til Google Analytics fejler eller GTM ikke bliver loaded korrekt på siden, kan det betyde at sideskiftet aldrig bliver lavet.</p></li>
<li><p>Hvis brugeren holder CTRL (eller CMD på Mac) nede mens der klikkes på linket for at åbne det i en ny tab, skal der ikke laves et sideskift, da brugeren jo netop gerne vil blive på siden.</p></li>
</ol>

<p>Den første kan løses ved at lave et <a href="https://www.simoahava.com/gtm-tips/use-eventtimeout-eventcallback/" rel="noopener noreferrer" target="_blank">timeout på fx 2 sekunder</a>, så sideskiftet bliver lavet uanset hvad, hvis requestet til Google Analytics ikke er gennemført efter 2 sekunder.</p>

<p>Den anden kan løses ved at tjekke om click eventet har enten <code>event.ctrlKey</code> (Windows) eller <code>event.metaKey</code> (Mac) for at tjekke om brugeren holder CTRL/CMD nede mens der klikkes.</p>

<p>Alt logikken tilføjes til det <code>dataLayer.push</code> som udføres når brugeren klikker på et blogindlæg på en liste, fx forsiden.</p>

<pre><code class="language-javascript">dataLayer.push({
  event: "productClick",
  ecommerce: {
    click: {
      actionField: { list: pageType },
      products: [
        {
          name: title,
          id: id,
          price: price,
          brand: author,
          category: category,
          variant: year,
          position: position
        }
      ]
    }
  },
  eventCallback: function() {
    if (!e.ctrlKey &amp;&amp; !e.metaKey) {
      window.location = href;
    }
  },
  eventTimeout: 2000
});
</code></pre>

<h2>Brugbar CTR på produktlister</h2>

<p>Når der er styr på tracking af blogindlæg som har været på brugerens skærm i 2 sekunder, samt kliks, fås en meget mere brugbar CTR for de enkelte blogindlæg på de forskellige lister.</p>

<p>Brugbar fordi jeg ved at brugeren har haft tid til at læse overskriften og vurdere om blogindlægget er spændende og relevant. Det er helt afgørende for at man faktisk kan konkludere noget på baggrund af CTR.</p>

<h2>Brugerne klikker ikke på relaterede og nyeste indlæg</h2>

<p>Jeg har brugt Enhanced Ecommerce til at tracke min blog siden 2016. Da jeg gav bloggen et redesign i starten af 2019 undersøgte jeg hvor mange der klikker, når der vises relaterede indlæg i bunden af et indlæg eller klikker på listen af nyeste blogindlæg.</p>

<figure><a href="/assets/images/2019/08/Nyeste-blogindl%C3%A6g-imressions-clicks-CTR.png"><img src="/assets/images/2019/08/Nyeste-blogindl%C3%A6g-imressions-clicks-CTR.png" alt="Sidebar med nyeste blogindlæg - men klikker folk på dem?" width="1270" height="526" class="size-full wp-image-1828" /></a><figcaption>Sidebar med nyeste blogindlæg - men klikker folk på dem?</figcaption></figure>

<figure><a href="/assets/images/2019/08/Relaterede-blogindl%C3%A6g.png"><img src="/assets/images/2019/08/Relaterede-blogindl%C3%A6g.png" alt="I bunden af alle blogindlæg vises links til relaterede blogindlæg." width="941" height="595" class="size-full wp-image-1829" /></a><figcaption>I bunden af alle blogindlæg vises links til relaterede blogindlæg.</figcaption></figure>

<p>Det gør de ikke.</p>

<p>Slet ikke.</p>

<figure><a href="/assets/images/2019/08/Product-list-performance-nyeste-og-relaterede-indl%C3%A6g.png"><img src="/assets/images/2019/08/Product-list-performance-nyeste-og-relaterede-indl%C3%A6g.png" alt="CTR på 0,05% og 0,42% viser at meget få klikker på de links." width="1094" height="485" class="size-full wp-image-1830" /></a><figcaption>CTR på 0,05% og 0,42% viser at meget få klikker på de links.</figcaption></figure>

<p>Bemærk de meget forskellige antal impressions. Som beskrevet ovenfor tracker jeg kun impressions når links er synlige på skærmen og brugeren ikke har scrollet i 2 sekunder.</p>

<p>Nyeste indlæg vises i højre side højt oppe på siden, mens relaterede indlæg vises i bunden af blogindlæg, så der er langt færre der scroller helt ned til dem.</p>

<p>Fordi der er meget få kliks er det svært at optimere ud fra. Men hvis der havde været nogle flere kliks, ville det være oplagt at kigge på hvilke blogindlæg der fungerer godt når de vises som relaterede indlæg:</p>

<figure><a href="/assets/images/2019/08/Product-list-performance-products.png"><img src="/assets/images/2019/08/Product-list-performance-products.png" alt="CTR for de enkelte blogindlæg når de vises som relaterede indlæg." width="890" height="610" class="size-full wp-image-1833" /></a><figcaption>CTR for de enkelte blogindlæg når de vises som relaterede indlæg.</figcaption></figure>

<p>CTR på de links var dermed så lav, at de for langt de fleste brugere ikke er brugbare links, og dermed blot støj. Jeg valgte derfor at fjerne dem i det nye design og dermed få et mere clean design.</p>

<blockquote><p>Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.<cite>Antoine de Saint-Exupery</cite></p></blockquote>

<p>Til sammenligning har links på forsiden en CTR på 4,92%.</p>

<figure><a href="/assets/images/2019/08/Product-list-performance-homepage-og-forsiden.png"><img src="/assets/images/2019/08/Product-list-performance-homepage-og-forsiden.png" alt="CTR på forsiden." width="818" height="313" class="size-full wp-image-1844" /></a><figcaption>CTR på forsiden.</figcaption></figure>

<p>Screenshottet viser i øvrigt en kritisk vigtig ting i Enhanced Ecommerce og analytics generelt: <strong>Konsistent data</strong>.</p>

<p>Da jeg redesignede bloggen omskrev jeg alt JavaScript fra bunden, så det var skrevet i ren JavaScript (dvs. uden jQuery) og samtidig fulgte <a href="https://css-tricks.com/how-do-you-structure-javascript-the-module-pattern-edition/" rel="noopener noreferrer" target="_blank">den samme gode kodestruktur</a>.</p>

<p>Det betød desværre at jeg kom til at omdøbe Forsidens <code>product list name</code> fra Forsiden til homepage og dermed er data nu splittet.</p>

<p>Doh!</p>

<p>Lesson learned.</p>

<h2>Produkt detaljevisning, add to cart og checkout</h2>

<p>Okay, nu skal vi videre ned gennem tragten. Næste skridt fra <code>product click</code> er <code>detail view</code>. Nu gennemgår jeg alt det der sker på et blogindlæg.</p>

<p>Når et blogindlæg vises starter jeg med at sætte en række variabler, som bruges til at styre scroll trackingen. Der sættes det samme scrollTimeout på 2000 millisekunder som ved impressions og jeg sætter at brugeren mindst skal scrolle 150 pixels før de er begyndt at læse indlægget.</p>

<p>Derefter sættes en variabel til <code>false</code> for hvert event/state ned gennem siden. Når brugeren scroller til et punkt affyres et event og det sættes derefter til <code>true</code> så det samme event ikke trackes igen, hvis brugeren scrolle op igen.</p>

<p>Derefter vælges den <code>div</code> som indeholder blogindlægget, så jeg kan måle højden på den <code>div</code> og holde øje med hvor langt brugeren scroller. Bemærk at kommentarerne under indlægget ikke er med i denne <code>div</code>, så det er kun selve indlægget jeg kigger på.</p>

<p>Til sidst gemmes det aktuelle tidspunkt, som bruges til at afgøre hvor længe brugeren har været aktiv på siden.</p>

<pre><code class="language-javascript">// Default time delay before checking location
var scrollTimeout = 2000;

// # px before tracking a reader
var readerLocation = 150;

// Set some flags for tracking &amp; execution
var timer = 0;
var scroller = false;
var oneThird = false;
var twoThirds = false;
var endContent = false;
var didComplete = false;
var purchase = false;

// Content area DIV class
var contentArea = document.querySelector(".post-content");

// Set some time variables to calculate reading time
var startTime = new Date();
var beginning = startTime.getTime();
var totalTime = 0;
</code></pre>

<p>Derefter pushes en Enhanced Ecommerce action sat til <code>detail</code> som sendes med sidevisningen når brugeren lander på blogindlægget. Konsistens er vigtig i Enhanced Ecommerce, så de produktdata der sendes med her, skal være identiske med dem som bruges ved <code>impressions</code> og <code>click</code>.</p>

<pre><code class="language-javascript">// Track the article load as a Product Detail View
dataLayer.push({
   ecommerce: {
     detail: {
       products: product
     }
   }
});
</code></pre>

<p>Derefter defineres den funktion som affyres når brugeren ikke har scrollet i 2000 millisekunder.</p>

<pre><code class="language-javascript">// Check the location and track user
function trackLocation() {
  clearTimeout(scrollTimeout);

  scrollTimeout = setTimeout(function() {
// Herinde placeres alt koden som affyres efter 2000 millisekunder

    }
  }, 2000);
}

// Track the scrolling and track location
window.addEventListener("scroll", trackLocation);
},
</code></pre>

<p>Når brugeren begynder at scrolle på siden og dermed begynder at læse indholdet, trackes dette med et <code>add to cart</code> event. Her bruger jeg samme debounce funktion som tidligere, sat til 2 sekunder.</p>

<pre><code class="language-javascript">scrollTimeout = setTimeout(function() {
    bottom = window.innerHeight + window.pageYOffset;

    // If user starts to scroll send an event
    if (bottom &gt; readerLocation &amp;&amp; !scroller) {
      dataLayer.push({
        event: "addToCart",
        ecommerce: {
          add: {
            products: product
          }
        }
      });
      scroller = true;          
    }
</code></pre>

<p>Når brugeren lander på siden måles højden på artiklen i pixels, som bruges til at tracke hvor meget af artiklen der læses. Hvis brugeren scroller 33% af artiklen, trackes checkout step 1.</p>

<p>Ved 66% trackes step 2 og ved 100% af artiklen trackes step 3.</p>

<pre><code class="language-javascript">// If one third is reached
if (
  bottom &gt;= contentArea.offsetTop + contentArea.clientHeight / 3 &amp;&amp;
  !oneThird
) {
  dataLayer.push({
    event: "checkout",
    ecommerce: {
      checkout: {
        actionField: { step: 1, option: product[0].variant },
        products: product
      }
    }
  });
  oneThird = true;
}

// If two thirds is reached
if (
  bottom &gt;= contentArea.offsetTop + contentArea.clientHeight / 3 * 2 &amp;&amp;
  !twoThirds
) {
  dataLayer.push({
    event: "checkout",
    ecommerce: {
      checkout: {
        actionField: { step: 2, option: product[0].variant },
        products: product
      }
    }
  });
  twoThirds = true;          
}

// If user has hit the bottom of the content send an event
if (
  bottom &gt;= contentArea.offsetTop + contentArea.clientHeight &amp;&amp;
  (!endContent || !purchase)
) {
  if (!endContent) {
    dataLayer.push({
      event: "checkout",
      ecommerce: {
        checkout: {
          actionField: { step: 3, option: product[0].variant },
          products: product
        }
      }
    });
    endContent = true;
  }
}
</code></pre>

<h2>Tracking af læste blogindlæg som køb</h2>

<p>Hvis brugeren har været på siden mere end 1 minut, når der er scrollet 100% af artiklen, antages det at brugeren har læst artiklen og ikke bare skimmet den og den handling trackes som et køb. Prisen på ordren er antal ord i blogindlægget og dermed kan man se hvor mange artikler og ord der bliver læst på bloggen.</p>

<pre><code class="language-javascript">// If user has reached end of funnel, check if 60 seconds is passed
if (endContent &amp;&amp; !purchase) {
  currentTime = new Date();
  contentScrollEnd = currentTime.getTime();
  timeToContentEnd = Math.round((contentScrollEnd - beginning) / 1000);
  if (timeToContentEnd &gt; 60 &amp;&amp; !purchase) {
    // Track purchase
    dataLayer.push({
      event: "purchase",
      ecommerce: {
        purchase: {
          actionField: {
            id:
              new Date().getTime() +
              "_" +
              Math.random()
                .toString(36)
                .substring(5),
            revenue: product[0].price
          },
          products: product
        }
      }
    });

    // Only do this once!
    purchase = true;
  } else {
    dataLayer.push({
      event: "scrollToEndBeforeOneMinute",
      product: product[0].name
    });
  }
}
</code></pre>

<p>Den samlede kode for tracking af læsning af et blogindlæg ser dermed således ud:</p>

<pre><code class="language-javascript">// Track single post as product
trackSinglePostAsProduct: function(product) {
  // Default time delay before checking location
  var scrollTimeout = 2000;

  // # px before tracking a reader
  var readerLocation = 150;

  // Set some flags for tracking &amp; execution
  var timer = 0;
  var scroller = false;
  var oneThird = false;
  var twoThirds = false;
  var endContent = false;
  var didComplete = false;
  var purchase = false;

  // Content area DIV class
  var contentArea = document.querySelector(".post-content");

  // Set some time variables to calculate reading time
  var startTime = new Date();
  var beginning = startTime.getTime();
  var totalTime = 0;

  // Track the article load as a Product Detail View
  dataLayer.push({
    ecommerce: {
      detail: {
        products: product
      }
    }
  });

  // Check the location and track user
  function trackLocation() {
    clearTimeout(scrollTimeout);

    scrollTimeout = setTimeout(function() {
      bottom = window.innerHeight + window.pageYOffset;

      // If user starts to scroll send an event
      if (bottom &gt; readerLocation &amp;&amp; !scroller) {
        dataLayer.push({
          event: "addToCart",
          ecommerce: {
            add: {
              products: product
            }
          }
        });
        scroller = true;          
      }

      // If one third is reached
      if (
        bottom &gt;= contentArea.offsetTop + contentArea.clientHeight / 3 &amp;&amp;
        !oneThird
      ) {
        dataLayer.push({
          event: "checkout",
          ecommerce: {
            checkout: {
              actionField: { step: 1, option: product[0].variant },
              products: product
            }
          }
        });
        oneThird = true;
      }

      // If two thirds is reached
      if (
        bottom &gt;= contentArea.offsetTop + contentArea.clientHeight / 3 * 2 &amp;&amp;
        !twoThirds
      ) {
        dataLayer.push({
          event: "checkout",
          ecommerce: {
            checkout: {
              actionField: { step: 2, option: product[0].variant },
              products: product
            }
          }
        });
        twoThirds = true;          
      }

      // If user has hit the bottom of the content send an event
      if (
        bottom &gt;= contentArea.offsetTop + contentArea.clientHeight &amp;&amp;
        (!endContent || !purchase)
      ) {
        if (!endContent) {
          dataLayer.push({
            event: "checkout",
            ecommerce: {
              checkout: {
                actionField: { step: 3, option: product[0].variant },
                products: product
              }
            }
          });
          endContent = true;
        }
      }

      // If user has reached end of funnel, check if 60 seconds is passed
      if (endContent &amp;&amp; !purchase) {
        currentTime = new Date();
        contentScrollEnd = currentTime.getTime();
        timeToContentEnd = Math.round((contentScrollEnd - beginning) / 1000);
        if (timeToContentEnd &gt; 60 &amp;&amp; !purchase) {
          // Track purchase
          dataLayer.push({
            event: "purchase",
            ecommerce: {
              purchase: {
                actionField: {
                  id:
                    new Date().getTime() +
                    "_" +
                    Math.random()
                      .toString(36)
                      .substring(5),
                  revenue: product[0].price
                },
                products: product
              }
            }
          });

          // Only do this once!
          purchase = true;
        } else {
          dataLayer.push({
            event: "scrollToEndBeforeOneMinute",
            product: product[0].name
          });
        }
      }
    }, 2000);
  }

  // Track the scrolling and track location
  window.addEventListener("scroll", trackLocation);
},
</code></pre>

<p>Dataene kan blandt andet ses i Product Performance rapporten.</p>

<figure><a href="/assets/images/2020/07/Product-performance.jpg"><img src="/assets/images/2020/07/Product-performance-860x452.jpg" alt="Top 10 mest læste blogindlæg og deres gennemsnitspris (antal ord)." width="860" height="452" class="size-large wp-image-2329" /></a><figcaption>Top 10 mest læste blogindlæg og deres gennemsnitspris (antal ord).</figcaption></figure>

<h2>Analyse af Ecommerce data for min blog</h2>

<p>Okay, lad os kigge på det data jeg kan få ud af alt det her.</p>

<h3>Shopping behaviour</h3>

<p>En af de fedeste rapporter i Enhanced Ecommerce er Shopping Behaviour, som viser en komplet funnel over hele websitet fra total antal sessioner til antal køb.</p>

<p>Her ses frafaldet i hvert step mod læste blogindlæg.</p>

<figure><a href="/assets/images/2019/08/Shopping-behaviour.png"><img src="/assets/images/2019/08/Shopping-behaviour.png" alt="Shopping behaviour" width="1556" height="724" class="size-full wp-image-1926" /></a><figcaption>Shopping behaviour</figcaption></figure>

<p>Jeg kan se at en stor del af de besøgende ser blogindlæg (faktisk hele 96%) og rigtige mange begynder at scrolle (add to cart). 85% af dem der scroller når også ned til den første 1/3 af indlægget (checkout) men kun 20% af dem læser et blogindlæg. Der er et stort frafald på det sidste step.</p>

<p>Det kigger vi lige nærmere på med <code>Checkout behaviour</code>.</p>

<figure><a href="/assets/images/2019/08/Checkout-behaviour.png"><img src="/assets/images/2019/08/Checkout-behaviour.png" alt="Checkout behaviour" width="1285" height="719" class="size-full wp-image-1927" /></a><figcaption>Checkout behaviour</figcaption></figure>

<p>Antal sessioner bliver cirka halveret i hvert step, men dog er 78% af dem som scroller helt til bunden også på siden længe nok, til at de læser blogindlægget og tracket som et køb.</p>

<h3>Ekskludering af irrelevante blogindlæg</h3>

<p>Mit mest besøgte blogindlæg er uden sammenligning min <a href="/hvor-meget-drikker-gaesterne-til-et-bryllup/">infografik over hvor meget der blev drukket til vores bryllup</a>.</p>

<figure><a href="/assets/images/2019/08/Mest-viste-sider-GDS-chart.png"><img src="/assets/images/2019/08/Mest-viste-sider-GDS-chart.png" alt="Mest besøgte sider siden 2009." width="823" height="415" class="size-full wp-image-1887" /></a><figcaption>Mest besøgte sider siden 2009.</figcaption></figure>

<p>Jeg har brugt Enhanced Ecommerce til at tracke min blog siden december 2016 og siden da har den infografik stået for 77% af alle sidevisninger.</p>

<figure><a href="/assets/images/2019/08/drinksregnskab-77-procent-sidevisninger-siden-2016.png"><img src="/assets/images/2019/08/drinksregnskab-77-procent-sidevisninger-siden-2016.png" alt="Infografikken står for 77% af alle sidevisninger på sitet." width="741" height="351" class="size-full wp-image-1888" /></a><figcaption>Infografikken står for 77% af alle sidevisninger på sitet.</figcaption></figure>

<p>Målgruppen og adfærden på det blogindlæg er markant anderledes end de andre blogindlæg jeg skriver om digital marketing, så derfor udelukker jeg den med et segment, i alle de nedenstående analyser.</p>

<h3>Top 10 blogindlæg</h3>

<p>Herunder ses top 10 blogindlæg baseret på sidevisninger (detail views) samt deres Buy-to-Detail Rate.</p>

<p>Eller sagt på en anden måde: En vanity metric mod en engagement metric.</p>

<p>Bemærk de kæmpe forskelle i engagement!</p>

<figure><a href="/assets/images/2020/07/Top-10-blogposts-buy-to-detail-rate.jpg"><img src="/assets/images/2020/07/Top-10-blogposts-buy-to-detail-rate-860x380.jpg" alt="Der er kæmpe forskel på hvor mange der rent faktisk læser blogindlæggene." width="860" height="380" class="size-large wp-image-2331" /></a><figcaption>Der er kæmpe forskel på hvor mange der rent faktisk læser blogindlæggene.</figcaption></figure>

<h3>Konverteringsrate pr. trafikkilder</h3>

<p>Med infografikken fjernet, kan jeg se om besøg fra forskellige kilder egentlig læser mine blogindlæg.</p>

<p>Gennemsnittet for sitet er en konverteringsrate på 26,58% hvilket vil sige at 27% af trafikken læser mindst ét blogindlæg. Det er jeg egentlig godt tilfreds med.</p>

<ul>
<li>Organisk trafik har en konvertering på 24,33% dvs. tæt på gennemsnittet.</li>
<li>Social er høj hvor 34% læser blogindlægget når det bliver delt.</li>
<li>E-mail er ekstremt høj hvor 43% læser blogindlægget. Næsten dobbelt så højt som gennemsnittet. Jeg sender kun e-mails ud, når jeg skriver nye blogindlæg, så det giver god mening at folk kun klikker på links i de e-mails, hvis de synes blogindlægget ser spændende ud. Men alligevel :)</li>
</ul>

<figure><a href="/assets/images/2019/08/Konvertering-for-default-channel-grouping.png"><img src="/assets/images/2019/08/Konvertering-for-default-channel-grouping.png" alt="Konvertering fordelt på trafikkilder." width="1276" height="511" class="size-full wp-image-1893" /></a><figcaption>Konvertering fordelt på trafikkilder.</figcaption></figure>

<p>Lad os først lige kigge nærmere på social og de posts jeg selv laver, når jeg har skrevet et nyt blogindlæg.</p>

<figure><a href="/assets/images/2019/08/Source-medium-sociale-posts.png"><img src="/assets/images/2019/08/Source-medium-sociale-posts.png" alt="Konvertering er markant højere end gennemsnittet på 27%." width="1268" height="351" class="size-full wp-image-1892" /></a><figcaption>Konvertering er markant højere end gennemsnittet på 27%.</figcaption></figure>

<p>Konverteringen her er markant højere end gennemsnittet på 27% men det er interessant at facebook konvertere lavere end de andre. Jeg poster typisk kun i <a href="https://www.facebook.com/groups/googleanalytics/" rel="noopener noreferrer" target="_blank">Analytics-nørder - den hårde kerne</a> hvor alle er interesseret i Analytics. På <a href="https://www.linkedin.com/in/jacobworsoe/" rel="noopener noreferrer" target="_blank">LinkedIn</a> og <a href="https://twitter.com/jacobworsoe" rel="noopener noreferrer" target="_blank">Twitter</a> ryger den bredt ud til mit netværk, som nok er en lidt mere blandet skare, men til trods for det, så er der flere der læser hele indlægget.</p>

<h2>Bliver blogindlæg læst eller bare skimmet?</h2>

<p>Hvis brugeren scroller helt til bunden af et blogindlæg inden der er gået 60 sekunder, har brugeren kun skimmet blogindlægget. Der er ikke noget godt Enhanced Ecommerce event der passer til det, så det derfor tracker jeg det blot som et normalt Event.</p>

<p>Jeg laver 3 segmenter, som allesammen har en detaljevisning i deres session:</p>

<ol>
<li>Sessioner som kun skimmer</li>
<li>Sessioner som kun læser</li>
<li>Sessioner som både skimmer og læser</li>
</ol>

<p>De tre segmenter kan brydes ned på device og dermed se adfærden.</p>

<figure><a href="/assets/images/2019/08/Andel-der-skimmer-eller-l%C3%A6ser-fordelt-p%C3%A5-devices.png"><img src="/assets/images/2019/08/Andel-der-skimmer-eller-l%C3%A6ser-fordelt-p%C3%A5-devices.png" alt="Andel der skimmer og læser fordelt på devices" width="1450" height="715" class="size-full wp-image-1919" /></a><figcaption>Andel der skimmer og læser fordelt på devices</figcaption></figure>

<ul>
<li>Der er altså <strong>26% der kun skimmer et blogindlæg</strong>, mens hele <strong>63% læser blogindlægget</strong> uden at skimme det først. Det er overraskende. Jeg havde egentlig forventet at langt flere startede med at skimme og derefter læse, hvis det så spændende ud - fx. masser af billede og ikke bare wall of text. Men det er faktisk kun 11% der først skimmer og derefter læser indlægget.</li>
<li>Det er dem som scroller helt til bunden inden der er gået et minut, og derefter bliver på siden og stadig er aktive (dvs. scroller) når der er gået et minut.</li>
<li>Det er derimod ikke overraskende at der er næsten <strong>dobbelt så mange der skimmer på desktop</strong> i forhold til mobile devices, da det er meget nemmere at scrolle ned i bunden på en desktop, fx med scroll-hjulet på musen eller "page down"-tasten. Det er lidt tungere at scrolle et langt indlæg igennem med swipe på en telefon.</li>
</ul>

<h2>Bliver lange blogindlæg læst mere end korte blogindlæg?</h2>

<p>I <code>Product Performance</code> rapporten kan du se <code>Average price</code> og <code>But-to-detail rate</code>. Med de to tal kan du se sammenhængen mellem blogindlæggets længde (prisen) og sandsynligheden for at det bliver læst.</p>

<p>Du plotter tallene på et Scatter Plot i Excel og tilføjer en trendlinje, som viser sammenhængen.</p>

<figure><a href="/assets/images/2019/08/Korrelation-mellem-pris-og-konvertering.png"><img src="/assets/images/2019/08/Korrelation-mellem-pris-og-konvertering.png" alt="Korrelationen mellem pris og konvertering er -0,32" width="1023" height="703" class="size-full wp-image-1922" /></a><figcaption>Korrelationen mellem pris og konvertering er -0,32</figcaption></figure>

<p>Trendlinjen viser en tydelig nedadgående sammenhæng mellem pris og konvertering, så jo længere blogindlægget er, jo mindre sandsynlighed er der for at det bliver læst til ende.</p>

<h3>Antal ord i buckets</h3>

<p>Du kan også inddele blogindlæggene i buckets af antal ord, fx 0-500, 501-1000, etc. og finde den optimale længde på et blogindlæg hvor brugerne oftest læser det hele.</p>

<p><a href="/assets/images/2019/11/Buy-to-Detail-Rate-vs.-Antal-ord.jpg"><img src="/assets/images/2019/11/Buy-to-Detail-Rate-vs.-Antal-ord-860x513.jpg" alt="" width="860" height="513" class="alignnone size-large wp-image-2121" /></a></p>

<p>Overraskende nok er det de helt korte indlæg på mindre end 500 ord hvor færrest læser det hele. Der er et sweetspot omkring 500-1500 ord og ligesom det ses i ovenstående Scatter Plot, så falder fastholdelsen i de lange indlæg.</p>

<h2>Der er STOR forskel på blogindlæg</h2>

<p>Okay, lad os kigge på mine to seneste blogindlæg som eksempler.</p>

<p>Baseret på antal pageviews er de cirka lige populære.</p>

<p><a href="/assets/images/2019/11/retur-vs-aws-pageviews.jpg"><img src="/assets/images/2019/11/retur-vs-aws-pageviews.jpg" alt="" width="637" height="371" class="alignnone size-full wp-image-2110" /></a></p>

<p>Men pageviews er bare en vanity metric. Den fortæller intet om kvaliteten eller evnen til at fastholde brugeren.</p>

<p>Og de to blogindlæg er meget forskellige.</p>

<ul>
<li><a href="/returvarer-google-analytics/">Tracking af returvarer i Google Analytics (den ultimative guide 2019)</a> er en inspiration, men også noget som er en reference til senere brug og den er på 3668 ord.</li>
<li><a href="/aws-iot-button-google-analytics/">Tracking af kaffeforbrug med AWS IoT Button og Google Analytics</a> er en sjov use-case for Google Analytics, den er rimelig letlæst og man skal læse (eller skimme) det hele for at den er sjov. Den er kun på 1571 ord.</li>
</ul>

<h3>Buy-to-Detail Rate</h3>

<p>Den store forskel på de to blogindlæg ses tydeligt i Buy-to-Detail rate som er 11,69% for returvarer-indlægget mens den er hele 46,26% på AWS IoT-indlægget!</p>

<p><a href="/assets/images/2019/11/retur-vs-aws-buy-to-detail.jpg"><img src="/assets/images/2019/11/retur-vs-aws-buy-to-detail-860x230.jpg" alt="" width="860" height="230" class="alignnone size-large wp-image-2113" /></a></p>

<p>Dvs. næsten halvdelen af alle dem som ser indlægget om AWS scroller helt til bunden og er mindst 1 minut på siden.</p>

<p>Men hvornår falder folk fra på returvarer-indlægget?</p>

<p>Men hey! Tabeller med rå data er måske fede for data scientists, men de dur ikke til at gøre data nemme at forstå. Så lad os lige lave en graf inden vi går videre.</p>

<figure><a href="/assets/images/2019/11/Fastholdelse-af-brugeren-i-et-blogindl%C3%A6g.jpg"><img src="/assets/images/2019/11/Fastholdelse-af-brugeren-i-et-blogindl%C3%A6g-860x437.jpg" alt="Fastholdelse af brugeren i et blogindlæg" width="860" height="437" class="size-large wp-image-2112" /></a><figcaption>Fastholdelse af brugeren i et blogindlæg</figcaption></figure>

<p>Meget bedre.</p>

<p>Herover ses en tydelig forskel hvor mange brugere på returvarer-indlægget starter med at scrolle (Add to cart) men meget få læser ned til 33% af indlægget (Checkout). Så de fleste har lige skimmet toppen og (forhåbentlig) bogmærket siden og så videre til andre ting.</p>

<p>På kaffe-indlægget er der slet ikke samme frafald, så det indlæg fastholder brugerne meget bedre. Det er godt at vide til fremtiden.</p>

<h2>Blog kategorier</h2>

<p>Der er også kæmpe forskel i fastholdelse af brugerne fordelt på kategorier. Indlæg om Nethandel bliver læst meget.</p>

<p>Heldigvis bliver mine indlæg om Webanalyse, som jeg lægger meget arbejde i, også læst meget, hvor 24% læser hele indlægget.</p>

<p>Til gengæld skal jeg vidst tage mig lidt sammen, når jeg skriver om SEO, som umiddelbart ikke er så interessante indlæg. Her har jeg også lige taget Hverdagsstatisk med, som er mit indlæg om <a href="/hvor-meget-drikker-gaesterne-til-et-bryllup/">drikkevarer til et bryllup</a>.</p>

<p><a href="/assets/images/2019/11/Buy-to-Detail-rate-for-kategorier.jpg"><img src="/assets/images/2019/11/Buy-to-Detail-rate-for-kategorier-860x597.jpg" alt="" width="860" height="597" class="alignnone size-large wp-image-2117" /></a></p>

<h2>Udgivelsesår</h2>

<p>Jeg skrev mit første blogindlæg på denne blog i 2009 og jeg har skrevet 35 indlæg i alt. Lad os se om jeg er blevet bedre til at skrive spændende indlæg igennem årene.</p>

<p><a href="/assets/images/2019/11/Buy-to-Detail-Rate-pr.-udgivelses%C3%A5r.jpg"><img src="/assets/images/2019/11/Buy-to-Detail-Rate-pr.-udgivelses%C3%A5r-860x502.jpg" alt="" width="860" height="502" class="alignnone size-large wp-image-2119" /></a></p>

<p>Jeg startede ret godt ud i 2009 og 2010 og havde derefter nogle knap så gode år, særligt 2014-2017. Men 2018 og 2019 har begge været rigtig gode år, så jeg skal vidst bare fortsætte med den type indlæg.</p>

<h2>Opsummering</h2>

<p>I det ovenstående har jeg gennemgået step-by-step hvordan jeg bruger Enhanced Ecommerce til at få et langt mere detaljeret billede af hvordan mit indhold performer.</p>

<p>Ikke bare vanity metrics, som pageviews, bounce rate og time on page.</p>

<p>Men metrics som viser præcist hvad brugerne gør på sitet, hvor lang tid de (korrekte) er på siden, samt hvor meget af indholdet de læser.</p>

<h2>Hvor mange procent tror du læser dette indlæg?</h2>

<p>Smid dit svar herunder og deltag i lodtrækning om en… ej pjat, du kan ikke vinde noget, men giv gerne et bud alligevel :)</p>]]></content><author><name></name></author><category term="Analytics" /><summary type="html"><![CDATA[Hvilke KPI’er er vigtige for en blog?]]></summary></entry><entry><title type="html">Workarounds på ITP</title><link href="https://www.jacobworsoe.dk/workarounds-paa-itp/" rel="alternate" type="text/html" title="Workarounds på ITP" /><published>2019-11-08T21:56:20+00:00</published><updated>2019-11-08T21:56:20+00:00</updated><id>https://www.jacobworsoe.dk/workarounds-paa-itp</id><content type="html" xml:base="https://www.jacobworsoe.dk/workarounds-paa-itp/"><![CDATA[<p>Der er mange mulige workarounds på ITP. Simo Ahava har <a href="https://www.simoahava.com/analytics/itp-2-1-and-web-analytics/" rel="noopener noreferrer">samlet en god oversigt her</a>.</p>

<p>Overordnet set kan workarounds deles op i to metoder:</p>

<ol>
   <li>Der gemmes et ID i localStorage (lukket af ITP 2.3)</li>
   <li>Der gemmes et ID i en server-side cookie</li>
</ol>

<p>Cookies der er sat server-side med <code>Set-Cookie</code> er pt. ikke berørt af ITP.</p>

<h2>Server-side cookies (HTTP cookies)</h2>

<p>Lad os lige se på forskellen mellem client-side og server-side cookies.</p>

<ul>
    <li>Client-side cookies sættes med JavaScript</li>
    <li>Server-side cookies kommer direkte fra serveren og bliver sat med Set-Cookie i HTTP headeren i det HTTP response der kommer retur fra serveren</li>
</ul>

<p>Her er hvad der sker når du skriver <a href="/">jacobworsoe.dk</a> i din browser.</p>

<ol>
<li>Browseren sender et HTTP request på en URL til serveren, fx. <a href="/">jacobworsoe.dk</a></li>
<li>Web serveren sender et HTTP response tilbage med HTML koden for dén URL. Et HTTP response indeholder nogle Headers som fx status koden (200, 301, 404, etc.), om siden skal caches i browseren og hvor længe, osv. Headeren kan også indeholde en Set-Cookie kommando som sætter en cookie i browseren.</li>
</ol>

<h2>Accutics Cookie Saver</h2>

<p>Måske har du hørt om <a href="https://cookiesaver.io/" rel="noopener noreferrer">Cookie Saver</a> fra danske Accutics. De kan sætte server-side cookies for dig.</p>

<p>Du opretter en CNAME record i din DNS hvor man peger et subdomæne på dit website på Cookie Saver’s server. I praksis fungerer det som en redirect.</p>

<p>Derefter indsætter du et stykke JavaScript i din Tag Manager, som sørger for at lave et request til det CNAME med en liste af de cookies du gerne vil have sat server-side. Cookie Saver sørger så for sætte sende et HTTP Response tilbage med de ønskede server-side cookies.</p>

<p>Det har nogle store fordele.</p>

<ol>
<li><p>Adblockere har ikke så nemt ved at blokere dette request, fordi subdomænet er unikt og kan hedde hvad som helst. Det er derimod meget nemt at blokere Google Analytics, fordi den altid sender til <code>collect.google-analytics.com</code>.</p></li>
<li><p>Når man laver et request til ens eget subdomæne for at sende data, er der mulighed for at serveren sætter en cookie via Set-Cookie som en del af det HTTP response der kommer retur fra serveren. Fordi requestet er lavet til ens eget domæne, kan serveren sætte en første parts server-side cookie, hvilket pt. ikke bliver blokeret af ITP.</p></li>
</ol>

<h2>Google kommer ikke med en "nem" løsning</h2>

<p>I hvert fald ikke hvis løsningen er server-side cookies. Det er simpelthen ikke teknisk muligt for Google at lave en simpel løsning på dette. For at sætte en server-side cookie kræver det at der laves et subdomæne på det website. Det kan Google ikke bare gøre for dig. Det skal du aktivt selv oprette.</p>

<p>Det betyder at du ikke skal sidde og vente på at Google løser det for dig. Det kan de ikke. Rent teknisk kan det ikke lade sig gøre.</p>

<p>Men samtidig betyder det også at Safari måske ikke begynder at blokere server-side cookies i de næste ITP versioner. Netop fordi det kræver en bevidst handling af dig.</p>

<p>En del af problemet som ITP prøver at løse er nemlig at der ofte bliver lavet en masse cross-site tracking af dine brugere - på det eget website - uden at du er klar over det. Bare fordi du har indsat en harmløst script på dit website.</p>

<p>Men med server-side cookies, sker der ikke noget uden du er klar over det, så det er ikke noget en 3. part bare kan gøre, hvilket formentlig er grunden til at ITP ikke blokerer det.</p>

<h2>Er workarounds spild af penge?</h2>

<p>Måske. Facebook <a href="/ekskluder-facebooks-fbclid-url-parameter-i-google-analytics/" rel="noopener noreferrer">forsøgte med et workaround</a> og det blev stoppet af ITP 2.1.</p>

<p>Det kan derfor sagtens være at det er spild af penge at implementere workarounds til ITP, som måske bliver blokeret i næste ITP version.</p>

<p>Du er nødt til at overveje hvor meget det koster dig at ITP ødelægger dine data.</p>

<p>Men som beskrevet ovenfor, så kommer Google ikke og løser det hele for dig, så det er værd at have med i overvejelserne i forhold til om det kan betale sig at lave en workaround.</p>]]></content><author><name></name></author><category term="Analytics" /><summary type="html"><![CDATA[Der er mange mulige workarounds på ITP. Simo Ahava har samlet en god oversigt her.]]></summary></entry><entry><title type="html">Googles dilemma med ITP</title><link href="https://www.jacobworsoe.dk/googles-dilemma-med-itp/" rel="alternate" type="text/html" title="Googles dilemma med ITP" /><published>2019-11-07T21:49:59+00:00</published><updated>2019-11-07T21:49:59+00:00</updated><id>https://www.jacobworsoe.dk/googles-dilemma-med-itp</id><content type="html" xml:base="https://www.jacobworsoe.dk/googles-dilemma-med-itp/"><![CDATA[<p>Lad os lige tage de store spillere i ITP set ud fra et web analytics perspektiv.</p>

<p><strong>Apple</strong>, som startede det hele med Safari browseren. Apple har ikke en annonceplatform og deres omsætning stammer ikke fra reklamer. De har ikke nogen Analytics platform. Apple kan sagtens prioritere brugernes privatliv, uden at skade deres egen forretning.</p>

<p><strong>Adobe</strong> har kun en <a href="https://business.adobe.com/products/analytics/adobe-analytics.html">analytics platform</a>. Ingen browser eller annoncer. Adobe skal kun tænke på at lave den bedst mulige Analytics platform, og kan sagtens <a href="https://experienceleague.adobe.com/docs/id-service/using/reference/analytics-reference/cname.html?lang=en#reference">lave workarounds til ITP</a>.</p>

<p><strong>Facebook</strong> har en annonceplatform hvor deres omsætning kommer fra og de har en analytics platform. Facebook skal lave den bedst mulige annonceringsplatform for deres annoncører som dermed bruger flere penge hos dem, og <a href="/ekskluder-facebooks-fbclid-url-parameter-i-google-analytics/">de laver derfor workarounds til ITP</a>.</p>

<p><strong>Google</strong> har både den mest brugte browser, den mest brugte web analytics platform og 95+% af deres omsætning kommer fra deres annonceplatform. De har en fod i alle lejre og skal træde meget varsomt.</p>

<p>Google vil gerne beskytte brugernes privatliv, fx ved at skjule organiske søgeord (not provided) eller muligheden for at Google automatisk løbende sletter ens aktivitetsdata.</p>

<figure><a href="/assets/images/2019/10/Choose-how-long-to-keep-your-Web-app-activity.jpg"><img src="/assets/images/2019/10/Choose-how-long-to-keep-your-Web-app-activity.jpg" alt="Google kan automatisk løbende slette dine aktivitetsdata." width="651" height="624" class="size-full wp-image-1975" /></a><figcaption>Google kan automatisk løbende slette dine aktivitetsdata.</figcaption></figure>

<p>Det er godt for deres image og godt for udbredelsen af Chrome, når de tager brugernes privatliv seriøst. Det er derfor ikke utænkeligt at Google vil introducere noget der minder om ITP i Chrome.</p>

<p>Firefox har introduceret <a href="https://blog.mozilla.org/blog/2019/09/03/todays-firefox-blocks-third-party-tracking-cookies-and-cryptomining-by-default/" rel="noopener noreferrer">Enhanced Tracking Prevention</a> (ETP), som også blokerer 3. parts cookies, samt en række af kendte tracking scripts, så der er ingen tvivl om at browserne bliver mere strikse med cookies og giver mere kontrol til brugeren.</p>

<p>Tidligere i år annoncerede Chrome at de <a href="https://blog.google/products/ads/transparency-choice-and-control-digital-advertising/" rel="noopener noreferrer">arbejder på en browser extension</a> til at vise hvilke data der er brugt til at personalisere de reklamer du ser, samt hvilke firmaer der har været involveret i databehandlingen. Google arbejder også på at brugeren får bedre <a href="https://blog.chromium.org/2019/05/improving-privacy-and-security-on-web.html" rel="noopener noreferrer">kontrol over 3. parts cookies</a> i Chrome.</p>

<p>Googles primære indtægtskilde er Google Ads og det er vigtigt at annoncørerne har nogle gode muligheder for at analysere deres data og optimere ad spend. Hvis de ikke kan se hvad der virker, er de ikke så tilbøjelige til at bruge penge på annoncering. Hvis ITP begrænser mulighederne for at analysere effekten af annonceringen, så er det et problem for Google.</p>

<p>Og til sidst har de Google Analytics, som selvfølgelig skal vise så korrekte data som muligt. Men er det sandsynligt at Google vil lancere en workaround til ITP, samtidig med at de skal beskytte brugernes privatliv? Og måske enda lancere noget lignende i Chrome? Det er muligt at Google blot er nødt til at acceptere at udviklingen går mod mindre tracking og bedre kontrol over brugernes data, og dermed ikke selv kommer med en løsning til ITP.</p>]]></content><author><name></name></author><category term="Analytics" /><summary type="html"><![CDATA[Lad os lige tage de store spillere i ITP set ud fra et web analytics perspektiv.]]></summary></entry></feed>