Pixel tracking – Ranker du nr. 1 på Google men er “below the fold”?

16. august 2020

Når du skal vide, hvor godt dit site klarer sig på Google, så bruger du sikkert et værktøj som Accuranker, Ahrefs eller Google Search Console til at se de organiske placeringer på dine søgeord. Rangerer vi nr. 1, så er det bare perfekt.

Men…

Du kan faktisk ligge nr. 1 på Google og være “below the fold”. Dvs. at laver du en Googlesøgning på din bærbare, så er du ikke synlig på de første 1.000 pixels på siden, og man skal scrolle ned, før dit resultat bliver synligt.

Her er et eksempel. Hvis du søger på “kjoler”, skal du 1.400 pixels ned for at finde det første organiske resultat.

Dr. Pete fra Moz har vist, at man skal hele 2.400 pixels ned på Google US for at finde det første organiske resultat for “vacuum cleaners”. Vi har endnu ikke amerikanske tilstande på Google DK, men det er kun et spørgsmål om tid.

Så hvis man arbejder med SEO er det vigtigt at forstå, at din topplacering i Google ikke må være en sovepude. Nedenfor gennemgår jeg det næste SEO slag, som du skal arbejde på at vinde. Det er nemmere, hvis man allerede ligger i toppen, men der er også plads til dem, som gør det rigtigt.

Din trofaste rank tracker giver dig ikke den fulde sandhed. Af mangel på bedre arbejder jeg i stedet med pixel tracking, som giver indsigter, hvor rank trackerne kommer til kort.

Senere i indlægget kan du selv få lov til at lege med data i en Pixel tracker, og vil du bygge én, så får du den manual, som vi selv bruger.

Lad os komme i gang

Lad os først se på, hvordan klikrater er fordelt ud fra placeringer på Google.

Ranker man på side 2 på Google for ens søgeord, er det, som det altid har været. Man får ingen kliks. Backlinko undersøgte i 2019 klikrater for 5 mio. søgninger på Google US. Kun 0,78% klikkede på resultater på side 2. De observerede også, at langt de fleste brugere ikke kommer længere ned end til placering 5 på side 1.

For IMPACT Extends egne kunder ser vi det samme billede. Her er et par eksempler på et par store omnichannel-spillere, hvor jeg har lavet en CTR analyse for deres non-brandede søgeord. X-aksen er placering på Google (fra nr. 1 til 10 fra venstre), og Y-aksen viser den gennemsnitlige CTR per placering:

Hvor Backlinkos studie viste en gennemsnitlig CTR på 30% for at ligge nr. 1, vil CTR i en branche med meget konkurrence i Danmark ligge omkring 10-15%. CTR falder drastisk for hver placering, og ligger man udenfor top 5, så taler vi om en CTR på 1-3%. Det betaler ikke huslejen.

Lad os gå ned på søgeordsniveauet for virkelig at forstå, hvilken CTR som man kan opleve. Se dette screenshot fra Google Search Console.

Vi rangerer i toppen af Google for et kommercielt søgeord, som har 3.000 søgninger om måneden. Der er godt fyldt op med Google Ads og Google Shopping, men det er også et søgeord, som resulterer i en del non-kliks grundet, at brugeren går til Google Image Search. Vi har primært rangeret nr. 1 de sidste 12 mdr., men CTR er kun 4,4%.

Jeg gentager lige.

4,4%!

Googles interesse i konstant at udvide Google Ads, alternativt insistere på at give svaret direkte på side 1 eller at sende trafik til deres egne platforme som Youtube og Google Maps har en klar effekt. Det gør, at fremtidens forventede CTR som nr. 1 mere vil se ud som ovenstående, end de 25-35% som Backlinko og andre lignende studier fortæller os.

Det er den nye virkelighed.

Som nævnt i starten er udviklingen af Google DK ikke så langt fremme som i de større lande, men det kommer helt sikkert. Og det gælder om at være parat.

Er du f.eks. indenfor rejsebranchen, og er Google en vigtig trafikkanal?

Så bare spørg Tripadvisor hvad de synes om Googles udvikling af Google Ads og de interne søgemaskiner Google Flights og Google Hotels.

 

Tripadvisor stock price

Hvordan SERP features udfordrer den klassiske tankegang

Siden 1998 har vi i SEO industrien været vant til at måle ud fra organiske placeringer. Det er altså 22 år siden nu, og Google har siden da gennemgået en transformation. De har flyttet sig fra at præsentere “Ten Blue Links” til at blive en portal. Det er vigtigt, at vi også flytter os til at se på andet end organiske placeringer.

Det engelske bureau ROAST lavede i 2019 en SERP analyse af, hvad Google vælger at vise brugerne på side 1 for mange tusinde forskellige søgninger. Ikke overraskende fyldte Google Ads 23% af al plads. De 10 organiske resultater fyldte derimod kun 50%. Det efterlader os med hele 27% af pladsen, som optages af SERP features. Fra førhen at være nye eksotiske muligheder er SERP features nu dér, hvor SEO slaget skal slås.

Hvad er en SERP feature?

En SERP feature er alt, som bliver vist på side 1 på Google, som ikke er betalte ads eller de standard 10 organiske resultater. Dvs, at det er elementer som både kan optræde i det enkelte søgeresultat (Rich snippets) eller mellem søgeresultater.

SEMRush har identificeret 25 SERP features, men de er ikke alle lige udbredte på Google DK.

Her er dem, som man oftest ser i Danmark:

Featured snippets (også kaldet placering 0. Man kan diskutere om det stadig er en SERP feature eller bare en fremhævet placering 1 af Google, men man skal stadig optimere mod den for at få en fremhævet placering uden konkurrentens billede)

3-pack eller Local pack (de tre fremhævede søgeresultater i Google Maps)

FAQ (Muligheden for at fylde langt mere med links til interne sider. Stadig meget populær)

Stjerner (også kaldet star ratings, som er anmeldelser i søgeresultatet. Som regel i form af fem strålende stjerner, som lyser side 1 op)

Sitelinks (vises under ens søgeresultat med links til specifikke steder på sitet)

Top Stories (nyhedskarussel fra udvalgte medier). Der kan også være en tilsvarende News, som er meget ens, men bliver kun vist, hvis der er et trending topic.

Videokarussel (En række instruktionsvideoer normalt med links til Youtube)

Af andre kan nævnes People Also Ask (liste over relaterede spørgsmål på engelsk som kan foldes ud i det uendelige, indtil brugeren får nok) og Image pack (Billeder fra Google Image Search som kan vises mellem søgeresultaterne). Men alle er i spil, og du kan blive first mover i din branche ved at se nærmere på Schema.org og SEMRush’ liste.
Der er ikke nogen restriktioner for, hvorfor dit site ikke kan være en del af disse SERP features, selvom Top Stories i 99% af tilfældene vil være større medier. Men så længe at Google viser dem for din type søgninger, så har du muligheder. Og selv når de ikke viser dem, så kan det være, fordi dine konkurrenter ikke fokuserer på SERP features (endnu).

Pixel tracking er løsningen i en verden domineret af SERP features

En klassisk rank tracker vil fortælle dig om din side 1 placering ud fra de 10 organiske placeringer. Dvs. at du ikke ved om du har SERP features i resultatet og over dit søgeresultat. Denne falske tryghed gør, at du tror, at det går godt, men at du kunne få langt flere kliks, hvis du udnyttede mulighederne bedre.

Hvordan kan vi gøre det nemmere at forstå, hvad der sker på Google og udnytte mulighederne?

Mit svar er pixel tracking.

Hvis vi kan måle, hvor langt vores resultat ligger fra toppen af skærmen dagligt i pixels, og vi samtidigt måler størrelsen af vores søgeresultat i pixels, så har vi en løsning på, hvordan man skal indlemme SERP features.

I perioden 16.03.20 til 30.03.20 trackede vi de daglige pixel bevægelser for 259 søgeord fra toppen af skærmen på desktop. Nedenfor kan du se de daglige bevægelser i perioden.

På X-aksen har vi datoerne for de to uger fra venstre til højre, og på Y-aksen har vi pixels fra toppen af skærmen, hvor 0 er lig med at ligge helt oppe i toppen af skærmen. Den stiplede linje ved 1.000 pixels markerer, om et søgeord er above eller below the fold.

Her kan du teste en simpel Pixel Tracker for 264 søgeord for diverse spillere i bankindustrien. Brug “viskelæderet” for at fjerne en filterfunktion.

  • Vælg søgeord i Query
  • Vælg site ved at vælge Domain
  • Se størrelsen af søgeresultatet fra dag til dag via den sekunddære Y-akse

 

 

Lad os dykke ned i nogle af søgeordene og forklare de daglige bevægelser med tre praktiske eksempler.

Eksempel 1 - Vi rangerer nr. 3, på dette søgeord, men en Image Pack sender vores site below the fold

Vi lægger blødt ud. Søgeordet “aftaleindlån” har i hele perioden ligget konstant nr. 3 i de organiske resultater i følge Accuranker. Med pixel tracking kan vi se, at der har været et fald på 600 pixels fra 27.3. Det skyldes, at en Image pack dukker op.

Eksempel 2 - Vi rangerer nr. 2 på et kæmpe søgeord, men sitet kom aldrig above the fold

I det næste eksempel kommer pulsen op.

Der var knap 5.000 søgninger på Boliglån i de 2 uger.

Serpwoo og Accuranker bekræfter begge, at sitet falder i placeringer på søgeordet. Accuranker siger fra 2 til 5 i perioden fra 21.3-30.3. Serpwoo siger, at de lå nr. 1 på søgeordet 19.3., faldt til nr. 4 20.3. og derefter nr. 5 til 30.03. I følge pixel trackeren har søgeordet ligget below the fold i hele perioden – også som nr. 2.

Det skyldes for det første, at der konstant ligger 3-4 Google Ads i toppen. men udover det så var boliglån et trending topic i perioden. Det affødte en News SERP feature på 283 pixels lige under Google Ads i hele perioden.

Hvis man kun ser på søgeordet via en rank tracker, ser man i den første periode en top 2 placering og alt er godt. Men i virkeligheden ligger søgeordet skjult for brugeren, som har stor betydning for CTR.

For dette søgeord er der kæmpe forskel i CTR ved at ligge nr. 1 eller nr. 2, og pixel trackin giver os indsigten til at forstå, at nr. 2 ikke er godt nok. Vi skal gå efter førstepladsen.

Eksempel 3 - Nr. 1 men en ny Featured snippet sender brutalt vores site below the fold

Sitet ligger nr. 1 på søgeordet “bedste bank” i hele perioden. Der ligger 4 ads i toppen til og med 26.3. Den 18.3. flyver der en Featured snippet på 409 pixels ind, og sammen med Google Ads sender den placering 1 ned below the fold. Op til weekenden den 27.3. og 28.3. ligger der kun én Google Ads og en Featured snippet. Den 29. forsvinder Google Ads og Featured snippet endeligt, og sitet ligger nu nr. 2 på skærmen. Dårligere rangering men højere oppe på skærmen.

Lidt af en rollercoaster i en periode, hvor rank trackerne viste, at sitet lå nr. 1. Og da de så viste, at sitet faldt en plads, så var det faktisk den mest sylige dag.

Med pixel tracking ville man få at vide, at sitet var rykket ned, og man kunne derefter beslutte sig til, om man skulle gå efter den Featured snippet, som altid ligger Above the fold.

Dags dato for dette blogindlæg ligger der igen en Featured snippet over folden, og vores placering 1 er tilbage i mørket.

Størrelsen af ens resultat

Som man kan se, får man flere indsigter ved at tilføje pixel tracking.

Det næste som jeg vil se på, er størrelsen af ens søgeresultat. Som nævnt tidligere er der flere muligheder for at tilføje Rich snippets. Nedenfor har jeg på min sekundære X-akse til højre målt den daglige størrelse af søgeresultatet. I dette eksempel varierer den mellem 75 og 95 pixels i perioden.

Men kan det udvides?

Ja.

Det resultat som fylder mest for “Forbrugslån” er nedenstående. Ved at tilføje sitelinks og stjerner kan vi vinde ekstra 50 pixels i højden. Det er en forøgelse på 52%!

Denne øvelse kan selvfølgelig gøres på et taktisk niveau ved at lave en Googlesøgning på det specifikke søgeord. Men med pixel tracking får du det store overblik, hvis du skal overskue flere hundreder eller tusinder af søgeord. Her har du mulighed for at filtrere på alle søgeord, som ligger indenfor de første 1.200 pixels, og som har en størrelse fra 75 til 95 pixels.

Er du blevet tændt på at få din egen pixel tracker? Vi kan selvfølgelig lave den for dig, men hvis du selv vil prøve kræfter, så tager min kollega Poomika dig igennem de forskellige trin nedenfor.

Sådan laver du din egen pixel tracker

Dette er en trin-for-trin guide til at pixel tracke dine egne søgeord dag-for-dag. Ved bare 2 steps, har du din Pixel Tracker klar til GO! Og du behøver overhovedet ikke at være en nørdet koder for at kunne følge med 😊

Nødvendige installationer og redskaber:

– Python 3 (Nyeste Python) installeret i Command
– PIP installeret i Command
– https://gitlab.com/databulle/python-authoritas-api-client
– Authoritas API nøgle
– Din liste af søgeord du gerne vil tracke
– Eventuelt Power Bi til visualisering

Før vi går i gang, vil jeg lige forklare lidt om udtrækket og API’et fra Authoritas. Med deres API har vi mulighed for at trække alle SERPs, der ligger i Google for et bestemt søgeord. For at kunne trække data gennem API’et skal vi først anmode om adgang, vente et par timer, og derefter kan vi få data. Dette gøres med to forskellige scripts, som køres med et par timers mellemrum. Det anbefales derfor, at lave en data request om morgenen og Get data om eftermiddagen. De to scripts er vedlagt i denne guide.

Lad os komme i gang.

Første trin er at sørge for, at du har den rigtige version af Python installeret. Dette tjekker du ved at gå ind i din Command (WIN+R, skriv ”cmd” eller søg efter Command på din computer) og skrive ”python -v”. Her skal den skrive; Python

I andet trin går du ind på Gitlab og installerer databulles repository (https://gitlab.com/databulle/python-authoritas-api-client). Dette repository skal du downloade til din computer. Hun har i sin pakke to Python scripts (send_queries.py og get_results.py). Disse scripts har vi modificeret, så vi kan trække det data ud, som vi skal benytte til at kunne tracke vores søgeord i pixels.

Før vi kan komme i gang med at requeste data skal vi have specificeret vores søgeord og installeret requirements.

Listen af søgeord

Din liste af søgeord skal du gemme i en .txt fil. Det er vigtigt at navngive denne fil ”your_queries”, og sørge for at alle søgeordene ligger linje-for-linje og ikke ved siden af hinanden i filen. Derudover skal du sørge for at gemme denne fil i mappen ”Python-authoritas-api-client-master”, som du lige har downloadet. Se her et eksempel på, hvordan du skal skrive søgeordene:

Config fil
Hvis du ikke allerede har oprettet en konto hos Authoritas, er det nu du skal gøre det. Vi skal bruge denne konto for at få de specifikke og unikke informationer for at kunne hente data. Det er:

Når informationen er hentet, skal de udfyldes i denne fil:

.CSV fil, ”job_ids”

Nu skal vi have oprettet en .CSV fil med navnet ”job_ids”. Denne fil skal bruges til at gemme hvert søgeords job_id. Alternativt kan du også omdøbe den fil, som det første script spytter ud, til ”job_ids”.

Installér requirements filen

Nu skal vi i gang med at benytte de filer, som allerede eksisterer i mappen. Før vi kan komme i gang med request data, skal vi have installeret de nødvendige krav, som scriptene skal bruge. Dette gør vi ved at gå i vores command og først finde vejen til vores ”python-authoritas-api-client-master” mappe. Det gør du ved at skrive ”cd + filepath”, se eksempel:

Dernæst skal du skrive ”dir”, for at se, om det er den rigtige mappe, som du har fundet. Ved at skrive ”dir” popper alt indhold i mappen op i din command.

Så skal vi have installeret requirements filen. Det gør du ved at skrive følgende:

pip install -r requirements.txt

Det kan tage et par minutter at installere.

Request data
Før vi kan komme i gang med request data, skal du gemme dette python script i mappen: python-authoritas-api-client-master. Det kan gemmes ved at kopiere alt ind i notepad og gemme filen med navnet: send_queries.py

Alternativt kan du erstatte den nuværende send_queries.py fil med dette script.

I scriptet har du mulighed for at specificere, hvilket land du er interesseret i at fetche data fra, hvilken enhed; pc, mobil, tablet osv., og hvor mange resultater du er interesseret i (om det skal være side 1,2,3 osv. i Google).

Script:


import requests
import time
import hmac
import hashlib
import base64
import json
import argparse
import csv
from configparser import ConfigParser

if __name__ == '__main__':

parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input', type=str, required=True,
help='Input file (one query per line).')
parser.add_argument('-o', '--output', type=str, default='queries',
help='Output file basename (default: queries).')
parser.add_argument('--sep', type=str, default=';',
help='Output CSV separator (default: ";").')
parser.add_argument('-n', '--nb_res', type=int, default=10,
help='Number of results to fetch (default: 10).')
parser.add_argument('-r', '--region', type=str, default='us',
choices=['global','fr','gb','us','es','dk'],
help='Region (default: us).')
parser.add_argument('-l', '--language', type=str, default='en',
choices=['en','fr','es','en'],
help='Language (default: en).')
parser.add_argument('-s', '--search_engine', type=str, default='google',
choices=['google','bing','yahoo','yandex','baidu'],
help='Search Engine (default: google).')
parser.add_argument('-u', '--user_agent', type=str, default='pc',
choices=['pc','mac','tablet','ipad','iphone','mobile'],
help='User Agent choice (default: pc).')
parser.add_argument('--no_cache', action='store_true', default=False,
help='Do not use query cache (default: False).')
parser.add_argument('-d', '--delay', type=int, default=4,
help='Delay in seconds between requests (default: 4).')
args = parser.parse_args()

# Get API settings from config.ini file
config = ConfigParser()
config.read('config.ini')
host = config.get('AUTHORITAS_API','host')
private_key = config.get('AUTHORITAS_API','private_key')
public_key = config.get('AUTHORITAS_API','public_key')
salt = config.get('AUTHORITAS_API','salt')

kws = list()
# Read list of requests
with open(args.input,'r', encoding='utf-8') as file:
for line in file.readlines():
kws.append(str.strip(line))
file.close()

# Output name
timestr = time.strftime("%Y%m%d-%H%M%S")
filename = args.output + "-" + timestr + ".csv"

output_headers = ['query','jid']
with open(filename,'w',newline='') as file:
writer = csv.DictWriter(file, fieldnames=output_headers, delimiter=args.sep)
writer.writeheader()
file.close()

for kw in kws:
# Generate a unix timestamp to insert into headers
now = int(time.time())

# Generate headers
hash_data = "{}{}{}".format(now, public_key, salt)
hashed = hmac.new(private_key.encode("utf-8"), hash_data.encode("utf-8"), hashlib.sha256).hexdigest()
headers = {
'accept': "application/json",
'Authorization': "KeyAuth publicKey={} hash={} ts={}".format(public_key,hashed,now)
}

# Request body
body = {
"search_engine":args.search_engine,
"region":args.region,
"language":args.language,
"max_results":args.nb_res,
"phrase":kw
}
if args.no_cache:
body['use_cache'] = False

url = '{}/search_results/'.format(host)
r = requests.post(url, data=json.dumps(body), headers=headers)
with open(filename,'a',newline='') as file:
writer = csv.DictWriter(file, fieldnames=output_headers, delimiter=args.sep)
writer.writerow({
'query': kw,
'jid': r.json().get('jid')
})
file.close()
print('Request {} created (jid: {}).'.format(kw,r.json().get('jid')))
time.sleep(args.delay)

Nu skal vi have request data på de søgeord, vi har specificeret i your_queries.txt filen. Det gør vi ved at skrive følgende i command (hvis du er gået ud af command siden, da vi installerede .requirements, skal du igen skrive ”cd + filepath” som tidligere, før du kan køre dette):

python send_queries.py –input your_queries.txt

Dette kan tage et par minutter afhængig af, hvor mange søgeord du har specificeret i din .txt fil. Resultatet af dette vil blive gemt i en .CSV fil i mappen: python-authoritas-api-client-master. Denne fil indeholder dine job id’er til hvert søgeord. Du kan enten omdøbe ”job_ids” eller kopiere værdierne fra filen ind i filen, som du tidligere oprettede med navnet ”job_ids”.

Get data
Efter et par timer (eller minutter afhængig af hvor mange søgeord du er interesseret i data for), kan vi .get data. Det gør vi med følgende script, som vi har modificeret. Ligesom med det første script kan du enten replace den fil, der hedder ”get_results.py” i mappen, eller du kan gemme denne på ny i notepad og gemme som en .py fil med navnet ”get_results”. Den anden skal omdøbes til ”get_results1.py”.

Dette er et langt script, men trækker al data ind fra Google – herunder pixels.

For at kunne køre dette script skal du skrive følgende i command (Hvis du igen har haft dit command vindue lukket, er det vigtigt, at du finder mappen i command ved at skrive: cd+filepath.):

python get_results.py –input job_ids.csv

Dette script spytter en excel, comma separeret fil ud, som indeholder alt det data, som vi skal bruge. Og ja du hørte rigtigt. Nu har vi alt vores data. 😀

Script


import requests
import time
import hmac
import hashlib
import base64
import json
import argparse
import csv
import configparser

if __name__ == '__main__':

parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input', type=str, required=True,
help='Input CSV file (needs columns query and jid).')
parser.add_argument('--input_sep', type=str, default=';',
help='Input CSV separator (default: ";").')
parser.add_argument('-o', '--output', type=str, default='results',
help='Output file basename (default: results).')
parser.add_argument('--sep', type=str, default=';',
help='Output CSV separator (default: ";").')
parser.add_argument('-d', '--delay', type=int, default=4,
help='Delay in seconds between requests (default: 4).')
args = parser.parse_args()

# Get API settings from config.ini file
config = configparser.ConfigParser()
config.read('config.ini')
host = config.get('AUTHORITAS_API','host')
private_key = config.get('AUTHORITAS_API','private_key')
public_key = config.get('AUTHORITAS_API','public_key')
salt = config.get('AUTHORITAS_API','salt')

# Output name
timestr = time.strftime("%Y%m%d-%H%M%S")
timestr1 = time.strftime("%Y%m%d")
filename = args.output + "-" + timestr + ".csv"
output_headers = ['query','jid','status','position','page','url','title','above_the_fold','top_left','bottom_right','description','type','rich_snippets','timestamp']
with open(filename,'w',newline='', encoding='utf-8') as output_file:
writer = csv.DictWriter(output_file, fieldnames=output_headers, delimiter=args.sep)
writer.writeheader()
output_file.close()

with open(args.input,'r') as input_file:
reader = csv.DictReader(input_file, delimiter=args.input_sep)
for row in reader:
kw = row['query']
jid = row['jid']

# Generate a unix timestamp to insert into headers
now = int(time.time())

# Generate headers
hash_data = "{}{}{}".format(now, public_key, salt)
hashed = hmac.new(private_key.encode("utf-8"), hash_data.encode("utf-8"), hashlib.sha256).hexdigest()
headers = {
'accept': "application/json",
'Authorization': "KeyAuth publicKey={} hash={} ts={}".format(public_key,hashed,now)
}
url = '{}/search_results/{}'.format(host,jid)

# Get data
r = requests.get(url, headers=headers)

with open(filename,'a',newline='', encoding='utf-8') as output_file:
writer = csv.DictWriter(output_file, fieldnames=output_headers, delimiter=args.sep)

if r.status_code == 200:
try:
response = json.loads(r.content)['response']['results']

for key in response['organic'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key,
'page': response['organic'][key]['page_number'],
'url': response['organic'][key]['url'],
'title': response['organic'][key]['title'],
'above_the_fold': response['organic'][key]['above_the_fold'],
'top_left': response['organic'][key]['top_left'],
'bottom_right': response['organic'][key]['bottom_right'],
'description': response['organic'][key]['description'],
'type': 'organic',
'rich_snippets': response['organic'][key]['rich_snippets'],
'timestamp': timestr1,
})

for key1 in response['image'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key1,
'page': response['image'][key1]['page_number'],
'url': response['image'][key1]['url'],
'title': response['image'][key1]['title'],
'above_the_fold': response['image'][key1]['above_the_fold'],
'top_left': response['image'][key1]['top_left'],
'bottom_right': response['image'][key1]['bottom_right'],
'description': response['image'][key1]['description'],
'type': 'image',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key2 in response['ad'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key2,
'page': response['ad'][key2]['page_number'],
'url': response['ad'][key2]['url'],
'title': response['ad'][key2]['title'],
'above_the_fold': response['ad'][key2]['above_the_fold'],
'top_left': response['ad'][key2]['top_left'],
'bottom_right': response['ad'][key2]['bottom_right'],
'description': response['ad'][key2]['description'],
'type': 'ad',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key3 in response['map'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key3,
'page': response['map'][key3]['page_number'],
'url': response['map'][key3]['url'],
'title': response['map'][key3]['title'],
'above_the_fold': response['map'][key3]['above_the_fold'],
'top_left': response['map'][key3]['top_left'],
'bottom_right': response['map'][key3]['bottom_right'],
'description': response['map'][key3]['description'],
'type': 'map',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key4 in response['people_also_ask'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key4,
'page': response['people_also_ask'][key4]['page_number'],
'url': response['people_also_ask'][key4]['url'],
'title': response['people_also_ask'][key4]['title'],
'above_the_fold': response['people_also_ask'][key4]['above_the_fold'],
'top_left': response['people_also_ask'][key4]['top_left'],
'bottom_right': response['people_also_ask'][key4]['bottom_right'],
'description': response['people_also_ask'][key4]['description'],
'type': 'people_also_ask',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key5 in response['shopping'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key5,
'page': response['shopping'][key5]['page_number'],
'url': response['shopping'][key5]['url'],
'title': response['shopping'][key5]['title'],
'above_the_fold': response['shopping'][key5]['above_the_fold'],
'top_left': response['shopping'][key5]['top_left'],
'bottom_right': response['shopping'][key5]['bottom_right'],
'description': response['shopping'][key5]['description'],
'type': 'shopping',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key6 in response['video'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key6,
'page': response['video'][key6]['page_number'],
'url': response['video'][key6]['url'],
'title': response['video'][key6]['title'],
'above_the_fold': response['video'][key6]['above_the_fold'],
'top_left': response['video'][key6]['top_left'],
'bottom_right': response['video'][key6]['bottom_right'],
'description': response['video'][key6]['description'],
'type': 'video',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key7 in response['destination'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key7,
'page': response['destination'][key7]['page_number'],
'url': response['destination'][key7]['url'],
'title': response['destination'][key7]['title'],
'above_the_fold': response['destination'][key7]['above_the_fold'],
'top_left': response['destination'][key7]['top_left'],
'bottom_right': response['destination'][key7]['bottom_right'],
'description': response['destination'][key7]['description'],
'type': 'destination',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key8 in response['direct_answer'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key8,
'page': response['direct_answer'][key8]['page_number'],
'url': response['direct_answer'][key8]['url'],
'title': response['direct_answer'][key8]['title'],
'above_the_fold': response['direct_answer'][key8]['above_the_fold'],
'top_left': response['direct_answer'][key8]['top_left'],
'bottom_right': response['direct_answer'][key8]['bottom_right'],
'description': response['direct_answer'][key8]['description'],
'type': 'direct_answer',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key9 in response['news'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key9,
'page': response['news'][key9]['page_number'],
'url': response['news'][key9]['url'],
'title': response['news'][key9]['title'],
'above_the_fold': response['news'][key9]['above_the_fold'],
'top_left': response['news'][key9]['top_left'],
'bottom_right': response['news'][key9]['bottom_right'],
'description': response['news'][key9]['description'],
'type': 'news',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key10 in response['knowledge_graph'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key10,
'page': response['knowledge_graph'][key10]['page_number'],
'url': response['knowledge_graph'][key10]['url'],
'title': response['knowledge_graph'][key10]['title'],
'above_the_fold': response['knowledge_graph'][key10]['above_the_fold'],
'top_left': response['knowledge_graph'][key10]['top_left'],
'bottom_right': response['knowledge_graph'][key10]['bottom_right'],
'description': response['knowledge_graph'][key10]['description'],
'type': 'knowledge_graph',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key11 in response['review'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key11,
'page': response['review'][key11]['page_number'],
'url': response['review'][key11]['url'],
'title': response['review'][key11]['title'],
'above_the_fold': response['review'][key11]['above_the_fold'],
'top_left': response['review'][key11]['top_left'],
'bottom_right': response['review'][key11]['bottom_right'],
'description': response['review'][key11]['description'],
'type': 'review',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key12 in response['social_media'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key12,
'page': response['social_media'][key12]['page_number'],
'url': response['social_media'][key12]['url'],
'title': response['social_media'][key12]['title'],
'above_the_fold': response['social_media'][key12]['above_the_fold'],
'top_left': response['social_media'][key12]['top_left'],
'bottom_right': response['social_media'][key12]['bottom_right'],
'description': response['social_media'][key12]['description'],
'type': 'social_media',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key13 in response['people_also_search_for'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key13,
'page': response['people_also_search_for'][key13]['page_number'],
'url': response['people_also_search_for'][key13]['url'],
'title': response['people_also_search_for'][key13]['title'],
'above_the_fold': response['people_also_search_for'][key13]['above_the_fold'],
'top_left': response['people_also_search_for'][key13]['top_left'],
'bottom_right': response['people_also_search_for'][key13]['bottom_right'],
'description': response['people_also_search_for'][key13]['description'],
'type': 'people_also_search_for',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key14 in response['recipe'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key14,
'page': response['recipe'][key14]['page_number'],
'url': response['recipe'][key14]['url'],
'title': response['recipe'][key14]['title'],
'above_the_fold': response['recipe'][key14]['above_the_fold'],
'top_left': response['recipe'][key14]['top_left'],
'bottom_right': response['recipe'][key14]['bottom_right'],
'description': response['recipe'][key14]['description'],
'type': 'recipe',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key15 in response['research_guide'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key15,
'page': response['research_guide'][key15]['page_number'],
'url': response['research_guide'][key15]['url'],
'title': response['research_guide'][key15]['title'],
'above_the_fold': response['research_guide'][key15]['above_the_fold'],
'top_left': response['research_guide'][key15]['top_left'],
'bottom_right': response['research_guide'][key15]['bottom_right'],
'description': response['research_guide'][key15]['description'],
'type': 'research_guide',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key16 in response['place'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key16,
'page': response['place'][key16]['page_number'],
'url': response['place'][key16]['url'],
'title': response['place'][key16]['title'],
'above_the_fold': response['place'][key16]['above_the_fold'],
'top_left': response['place'][key16]['top_left'],
'bottom_right': response['place'][key16]['bottom_right'],
'description': response['place'][key16]['description'],
'type': 'place',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key17 in response['hotel_finder'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key17,
'page': response['hotel_finder'][key17]['page_number'],
'url': response['hotel_finder'][key17]['url'],
'title': response['hotel_finder'][key17]['title'],
'above_the_fold': response['hotel_finder'][key17]['above_the_fold'],
'top_left': response['hotel_finder'][key17]['top_left'],
'bottom_right': response['hotel_finder'][key17]['bottom_right'],
'description': response['hotel_finder'][key17]['description'],
'type': 'hotel_finder',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key18 in response['flight_finder'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key18,
'page': response['flight_finder'][key18]['page_number'],
'url': response['flight_finder'][key18]['url'],
'title': response['flight_finder'][key18]['title'],
'above_the_fold': response['flight_finder'][key18]['above_the_fold'],
'top_left': response['flight_finder'][key18]['top_left'],
'bottom_right': response['flight_finder'][key18]['bottom_right'],
'description': response['flight_finder'][key18]['description'],
'type': 'flight_finder',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key19 in response['event_finder'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key19,
'page': response['event_finder'][key19]['page_number'],
'url': response['event_finder'][key19]['url'],
'title': response['event_finder'][key19]['title'],
'above_the_fold': response['event_finder'][key19]['above_the_fold'],
'top_left': response['event_finder'][key19]['top_left'],
'bottom_right': response['event_finder'][key19]['bottom_right'],
'description': response['event_finder'][key19]['description'],
'type': 'event_finder',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key20 in response['job_finder'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key20,
'page': response['job_finder'][key20]['page_number'],
'url': response['job_finder'][key20]['url'],
'title': response['job_finder'][key20]['title'],
'above_the_fold': response['job_finder'][key20]['above_the_fold'],
'top_left': response['job_finder'][key20]['top_left'],
'bottom_right': response['job_finder'][key20]['bottom_right'],
'description': response['job_finder'][key20]['description'],
'type': 'job_finder',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key21 in response['travel_finder'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key21,
'page': response['travel_finder'][key21]['page_number'],
'url': response['travel_finder'][key21]['url'],
'title': response['travel_finder'][key21]['title'],
'above_the_fold': response['travel_finder'][key21]['above_the_fold'],
'top_left': response['travel_finder'][key21]['top_left'],
'bottom_right': response['travel_finder'][key21]['bottom_right'],
'description': response['travel_finder'][key21]['description'],
'type': 'travel_finder',
'rich_snippets': ' ',
'timestamp': timestr1,
})

for key22 in response['refine_by'].keys():
writer.writerow({
'query': kw,
'jid': jid,
'status': 'ok',
'position': key22,
'page': response['refine_by'][key22]['page_number'],
'url': response['refine_by'][key22]['url'],
'title': response['refine_by'][key22]['title'],
'above_the_fold': response['refine_by'][key22]['above_the_fold'],
'top_left': response['refine_by'][key22]['top_left'],
'bottom_right': response['refine_by'][key22]['bottom_right'],
'description': response['refine_by'][key22]['description'],
'type': 'refine_by',
'rich_snippets': ' ',
'timestamp': timestr1,
})

print('Query "{}" OK (jid: {}).'.format(kw,jid))
# Some '200-OK' responses don't have real data
except KeyError:
writer.writerow({
'query': kw,
'jid': jid,
'status': 'error',
})
print('Error for query "{}" (jid: {}).'.format(kw,jid))
else:
writer.writerow({
'query': kw,
'jid': jid,
'status': r.status_code,
})
print('Error for query "{}" (jid: {}).'.format(kw,jid))
output_file.close()
time.sleep(args.delay)
input_file.close()

Du skal køre begge scripts dagligt/jævnligt. På den måde vil du kunne se bevægelserne for hver dag eller din egen specificerede periode. Vi bruger Power BI til at visualisere data. Alternative værktøjer kan også benyttes.

Rigtig god kodelyst 😊

Opsummering

Googles kontinuerlige forbedring af brugeroplevelsen på side 1 på Google – og forsøg på at øge Google Ads omsætningen – gør, at organiske resultater bliver skubbet længere ned. Denne udvikling vil fortsætte, så forvent at din CTR bliver reduceret.

Vi har indtil nu brugt de klassiske rank trackers til at måle, hvor godt at det går med søgeord. Men rangerer man først på side 1 på Google, så er der brug for mere information. Vi skal se på SERP features. SERP features vil fylde mere og mere, og det er her, at du kan vinde med fremtidens SEO.

Vores forsøg på at forstå performance på side 1 bedre er pixel tracking. Som jeg viste i eksemplerne ovenfor, vil en normal rank tracker ikke vise hele sandheden. Måling af pixels kan give os det ekstra lag af viden, som gør, at vi kan reagere, hvis der er ændringer i SERP Features. Vi vil vide, hvor vi skal se hen for at forbedre synligheden.

Faktum er, at jo højere vi ranker, og jo mere at vi fylder, jo højere bliver vores CTR og dermed relevant trafik til sitet. Det kræver måske kun få justeringer, hvis man allerede ligger i top 3, men vi har brug for indsigterne, som pixel tracking giver os, for at forbedre os.

Vil du booke en online gennemgang?