Hvilke KPI’er er vigtige for en blog?

Antal månedlige besøg er vigtig.

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

Men hvordan måles konvertering?

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

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.

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.

85% bounce rate.

85% bounce rate.

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.

Bliver indholdet rent faktisk læst?

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.

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.

Inspireret af Simo Ahava

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.

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: Track Content With Enhanced Ecommerce

Selve logikken i koden er kopieret fra Simo’s Github som er baseret på Justin Cutroni’s scroll script.

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: youmightnotneedjquery.com

Derudover har jeg lavet tre udvidelser af koden:

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

Mere om det senere.

Tracking af læsning af indhold med Enhanced Ecommerce

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.

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

  • Produkt: Et blogindlæg.
  • Produkt navn: Titlen på blogindlægget.
  • Produkt pris: Antal ord i blogindlægget.
  • Produkt kategori: WordPress kategori, fx “Webanalyse” eller “SEO”.
  • Produkt brand: Blogindlæggets udgivelsesår.
  • Produkt impression: Når et blogindlæg bliver vist på en liste, fx forsiden, kategorisider, tagsider, relateret blogindlæg eller lignende.
  • Produkt lister: Alle ovenstående lister.
  • Produkt click: Når en bruger klikker på et blogindlæg på en af ovenstående lister.
  • Produkt detail view: Når et blogindlæg vises (dvs. sidevisning af et blogindlæg).
  • Produkt add to cart: Når brugeren begynder at scrolle og dermed om de er begyndt at læse indlægget.
  • Produkt checkout: 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.
  • Gennemført købt: 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.

Lad os få styr på produktdata

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

ID og navnet (titlen) på indlægget kan fanges i WordPress i The Loop med de indbyggede funktioner the_title() og the_ID() og gemmes i en JavaScript variabel, så de kan sendes til Google Analytics. Årstallet som gemmes i brand hentes med the_time('Y').

Et indlæg kan tilhøre flere kategorier og hvis du bruger Yoast SEO 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:

// 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, & one is set
        $wpseo_primary_term = new WPSEO_Primary_Term( 'category', get_the_id() );
        $wpseo_primary_term = $wpseo_primary_term->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]->name;
            $category_link = get_category_link( $category[0]->term_id );
        } else { 
            // Yoast Primary category
            $category_display = $term->name;
            $category_link = get_category_link( $term->term_id );
        }
    } 
    else {
        // Default, display the first category in WP's list of assigned categories
        $category_display = $category[0]->name;
        $category_link = get_category_link( $category[0]->term_id );
    }

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

Derefter udskrives kategorien med <?php echo $category_display; ?>.

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

function word_count() {
    $content = get_post_field( 'post_content', $post->ID );
    $word_count = str_word_count( strip_tags( $content ) );
    return $word_count;
}

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 Product object med den korrekte syntaks ser således ud:

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
  }];

Product impressions efter 2 sekunder

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

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.

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

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

I værste fald kan det gøre sitet ubrugeligt, som det skete for Twitter tilbage i 2011.

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.John Resig, skaberen af jQuery

Begge ting kan løses med en debounce funktion.

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

Denne artikel fra CSS-Tricks.com har nogle gode visualiseringer og demoer som viser hvordan debounce virker. David Walsh har også skrevet om det her.

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.

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

Lad os kigge på koden.

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

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

Bemærk! Jeg lavede dette tilbage i 2016. Hvis det skal laves i dag, kan du med fordel bruge Intersection Observer 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. Browser support er rigtig god i dag, så den bør bruges fremover.

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

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
  });
}

Produktdataene er placeret i data attributter i HTML koden.

<h1 class="home-post-headline">
    <a href="https://www.jacobworsoe.dk/returvarer-google-analytics/" 
        data-title="Tracking af returvarer i Google Analytics (den ultimative guide)" 
        data-id="1597" 
        data-category="Webanalyse" 
        data-year="2019" 
        data-author="2" 
        data-price="3668" 
        data-position="2"
        class="home-post-link">
        Tracking af returvarer i Google Analytics (den ultimative guide)            
    </a>
</h1>

Den tredje funktion sender impressions til Google Analytics.

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

Ved pageload bygges et JavaScript object 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 pushProducts().

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

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

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

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

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

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.

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

Derefter defineres en funktion som køres hver gang der scrolles.

Det første funktionen gør, når der scrolles er clearTimeout som nulstiller timeren.

Derefter startes en ny setTimeout som indeholder den kode som køres efter 2000 millisekunder.

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.

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

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.

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

  function checkProductsInViewOnScroll() {
    clearTimeout(scrollTimeout);

    scrollTimeout = setTimeout(function() {
      for (var i = ga_products_not_visible.length - 1; i >= 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 > 0) {
        sendProducts("moreImpressionsSent");
      }
    }, 2000);
  }

  // Start scroll listener
  window.addEventListener("scroll", checkProductsInViewOnScroll);
}

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

// 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 < 0 || rect.top - viewHeight >= 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 && articles.length > 0) {
  for (var i = 0; i < 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 > 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 > 0) {
  var scrollTimeout;

  function checkProductsInViewOnScroll() {
    clearTimeout(scrollTimeout);

    scrollTimeout = setTimeout(function() {
      for (var i = ga_products_not_visible.length - 1; i >= 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 > 0) {
        sendProducts("moreImpressionsSent");
      }
    }, 2000);
  }

  // Start scroll listener
  window.addEventListener("scroll", checkProductsInViewOnScroll);
}

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.

Impressions fordelt på positioner på forsiden.

Impressions fordelt på positioner på forsiden.

Produkt click med et callback

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.

Løsningen er at annullere sideskiftet med JavaScript og istedet få GTM til at lave sideskiftet i et callback efter requestet er succesfuldt sendt til Google Analytics.

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

  1. Hvis requestet til Google Analytics fejler eller GTM ikke bliver loaded korrekt på siden, kan det betyde at sideskiftet aldrig bliver lavet.

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

Den første kan løses ved at lave et timeout på fx 2 sekunder, så sideskiftet bliver lavet uanset hvad, hvis requestet til Google Analytics ikke er gennemført efter 2 sekunder.

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

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

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 && !e.metaKey) {
      window.location = href;
    }
  },
  eventTimeout: 2000
});

Brugbar CTR på produktlister

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.

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.

Brugerne klikker ikke på relaterede og nyeste indlæg

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.

Sidebar med nyeste blogindlæg - men klikker folk på dem?

Sidebar med nyeste blogindlæg – men klikker folk på dem?

I bunden af alle blogindlæg vises links til relaterede blogindlæg.

I bunden af alle blogindlæg vises links til relaterede blogindlæg.

Det gør de ikke.

Slet ikke.

CTR på 0,05% og 0,42% viser at meget få klikker på de links.

CTR på 0,05% og 0,42% viser at meget få klikker på de links.

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.

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.

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:

CTR for de enkelte blogindlæg når de vises som relaterede indlæg.

CTR for de enkelte blogindlæg når de vises som relaterede indlæg.

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.

Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.Antoine de Saint-Exupery

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

CTR på forsiden.

CTR på forsiden.

Screenshottet viser i øvrigt en kritisk vigtig ting i Enhanced Ecommerce og analytics generelt: Konsistent data.

Da jeg redesignede bloggen omskrev jeg alt JavaScript fra bunden, så det var skrevet i ren JavaScript (dvs. uden jQuery) og samtidig fulgte den samme gode kodestruktur.

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

Doh!

Lesson learned.

Produkt detaljevisning, add to cart og checkout

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

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.

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

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

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

// Default time delay before checking location
var scrollTimeout = 2000;

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

// Set some flags for tracking & 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;

Derefter pushes en Enhanced Ecommerce action sat til detail 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 impressions og click.

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

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

// 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);
},

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

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

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

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.

Ved 66% trackes step 2 og ved 100% af artiklen trackes step 3.

// If one third is reached
if (
  bottom >= contentArea.offsetTop + contentArea.clientHeight / 3 &&
  !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 >= contentArea.offsetTop + contentArea.clientHeight / 3 * 2 &&
  !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 >= contentArea.offsetTop + contentArea.clientHeight &&
  (!endContent || !purchase)
) {
  if (!endContent) {
    dataLayer.push({
      event: "checkout",
      ecommerce: {
        checkout: {
          actionField: { step: 3, option: product[0].variant },
          products: product
        }
      }
    });
    endContent = true;
  }
}

Tracking af læste blogindlæg som køb

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.

// If user has reached end of funnel, check if 60 seconds is passed
if (endContent && !purchase) {
  currentTime = new Date();
  contentScrollEnd = currentTime.getTime();
  timeToContentEnd = Math.round((contentScrollEnd - beginning) / 1000);
  if (timeToContentEnd > 60 && !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
    });
  }
}

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

// 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 & 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 > readerLocation && !scroller) {
        dataLayer.push({
          event: "addToCart",
          ecommerce: {
            add: {
              products: product
            }
          }
        });
        scroller = true;          
      }

      // If one third is reached
      if (
        bottom >= contentArea.offsetTop + contentArea.clientHeight / 3 &&
        !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 >= contentArea.offsetTop + contentArea.clientHeight / 3 * 2 &&
        !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 >= contentArea.offsetTop + contentArea.clientHeight &&
        (!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 && !purchase) {
        currentTime = new Date();
        contentScrollEnd = currentTime.getTime();
        timeToContentEnd = Math.round((contentScrollEnd - beginning) / 1000);
        if (timeToContentEnd > 60 && !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);
},

Dataene kan blandt andet ses i Product Performance rapporten.

Top 10 mest læste blogindlæg og deres gennemsnitspris (antal ord).

Top 10 mest læste blogindlæg og deres gennemsnitspris (antal ord).

Analyse af Ecommerce data for min blog

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

Shopping behaviour

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.

Her ses frafaldet i hvert step mod læste blogindlæg.

Shopping behaviour

Shopping behaviour

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.

Det kigger vi lige nærmere på med Checkout behaviour.

Checkout behaviour

Checkout behaviour

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.

Ekskludering af irrelevante blogindlæg

Mit mest besøgte blogindlæg er uden sammenligning min infografik over hvor meget der blev drukket til vores bryllup.

Mest besøgte sider siden 2009.

Mest besøgte sider siden 2009.

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.

Infografikken står for 77% af alle sidevisninger på sitet.

Infografikken står for 77% af alle sidevisninger på sitet.

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.

Top 10 blogindlæg

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

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

Bemærk de kæmpe forskelle i engagement!

Der er kæmpe forskel på hvor mange der rent faktisk læser blogindlæggene.

Der er kæmpe forskel på hvor mange der rent faktisk læser blogindlæggene.

Konverteringsrate pr. trafikkilder

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

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.

  • Organisk trafik har en konvertering på 24,33% dvs. tæt på gennemsnittet.
  • Social er høj hvor 34% læser blogindlægget når det bliver delt.
  • 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 :)
Konvertering fordelt på trafikkilder.

Konvertering fordelt på trafikkilder.

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.

Konvertering er markant højere end gennemsnittet på 27%.

Konvertering er markant højere end gennemsnittet på 27%.

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 Analytics-nørder – den hårde kerne hvor alle er interesseret i Analytics. På LinkedIn og Twitter 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.

Bliver blogindlæg læst eller bare skimmet?

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.

Jeg laver 3 segmenter, som allesammen har en detaljevisning i deres session:

  1. Sessioner som kun skimmer
  2. Sessioner som kun læser
  3. Sessioner som både skimmer og læser

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

Andel der skimmer og læser fordelt på devices

Andel der skimmer og læser fordelt på devices

  • Der er altså 26% der kun skimmer et blogindlæg, mens hele 63% læser blogindlægget 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.
  • 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.
  • Det er derimod ikke overraskende at der er næsten dobbelt så mange der skimmer på desktop 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.

Bliver lange blogindlæg læst mere end korte blogindlæg?

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

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

Korrelationen mellem pris og konvertering er -0,32

Korrelationen mellem pris og konvertering er -0,32

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.

Antal ord i buckets

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.

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.

Der er STOR forskel på blogindlæg

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

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

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

Og de to blogindlæg er meget forskellige.

Buy-to-Detail Rate

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!

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

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

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.

Fastholdelse af brugeren i et blogindlæg

Fastholdelse af brugeren i et blogindlæg

Meget bedre.

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å kaffe-indlægget er der slet ikke samme frafald, så det indlæg fastholder brugerne meget bedre. Det er godt at vide til fremtiden.

Blog kategorier

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

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.

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 drikkevarer til et bryllup.

Udgivelsesår

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.

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.

Opsummering

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.

Ikke bare vanity metrics, som pageviews, bounce rate og time on page.

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.

Hvor mange procent tror du læser dette indlæg?

Smid dit svar herunder og deltag i lodtrækning om en… ej pjat, du kan ikke vinde noget, men giv gerne et bud alligevel :)