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.
I 2013 flyttede jeg bloggen over på WordPress hvor alt HTML blev konverteret til WordPress templates, men designet forblev nogenlunde uændret.
I 2014 lavede jeg designet responsivt så siden blev mobilvenlig.
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.
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).
Den anden del fokuserer på optimering med Google Analytics og opfølgning på effekten af de ændringer jeg har lavet.
Indhold
Part 1: Det tekniske med fokus på hvordan designet er kodet.
Part 2: Optimering af adfærden på sitet med Google Analytics data.
Mål med det nye design
- Mere moderne workflow til udvikling af websitet
- Oprydning i kode og sletning af unødvendige ting
- Hurtigere loadtid
- Højere konvertering
- Øge sidevisninger pr. besøg
Workflow
GruntJS
Der er mange (kedelige) opgaver involveret i at optimere front-end kode og jeg bruger GruntJS som task runner, til at udføre alle de opgaver automatisk.
GruntJS gør følgende ved mine filer:
- Samler JavaScript filer til én samlet, minificeret fil. Både de forskellige libraries jeg bruger og mine egne JavaScript filer.
- Compiler alle Sass filerne til en minificeret CSS fil.
- Kopiere alle de færdige optimerede filer over i en mappe, som indeholder de filer der skal uploades til webserveren.
- 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.
- 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.
Kun én JavaScript fil
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.
Browser caching og cache busting
JS filen caches i browseren i 1 år ved at sætte expire-headers til 1 år i .htaccess
. Jeg har brugt de anbefalede settings fra html5boilerplate.
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType application/x-javascript "access plus 1 year"
ExpiresByType text/javascript "access plus 1 year"
GruntJS laver et hash baseret på indholdet i filen og det hash bliver tilføjet til filnavnet.
Dvs. denne fil:
bundle.min.js
Kommer til at hedde:
bundle.min.e3d609e4.js
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.
Dermed kan jeg have filen cachet nærmest uendeligt, men stadig sikre at alle browsere får den nyeste fil, hvis jeg ændrer noget.
Gruntfile.js
For de interesserede, så er her min Gruntfile.js
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.
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"
]);
};
JavaScript
Tracking logik fra GTM til dataLayer
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 dataLayer
som det bør, men noget bliver også hurtigt tilføjet direkte i GTM for at afprøve det.
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 dataLayer
hvis det skal være permanent.
Væk med jQuery
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 youmightnotneedjquery.com en stor hjælp.
JavaScript reduceret fra 184 KB til 31 KB
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 FitVids.JS som jeg brugte da jeg lavede sitet responsivt 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.
Jeg indsætter en div
rundt om videoen.
<div class="videoWrapper">
<iframe src="//www.youtube.com/embed/usyYXNNBRjc" frameborder="0" allowfullscreen></iframe>
</div>
Og tilføjer lidt styling af den div
samt iframen som indeholder videoen, og så er videoen responsiv.
.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%
Jeg brugte også LunaMetrics’ script til tracking af visninger af YouTube videoer, men jeg brugte ikke den tracking til noget, så det blev også fjernet.
CSS
CSS skrevet i Sass
I det nye redesign skrev jeg alt CSS fra bunden igen og jeg valgte at skrive det i Sass.
Sass er et sprog som giver nogle ekstra muligheder og Sass filerne skal compiles til almindelige CSS filer inden de ryger ud på websitet.
Her er mine top 3 fedeste ting ved Sass.
1) Variabler. 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.
2) Imports. 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.
// 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"
3) Nesting. Med Nesting kan man tilføje underliggende selectors blot ved at indent’e linjen, så man slipper for at gentage selectors mange gange.
Her er et eksempel på Nesting i 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
Og her den CSS kode det compiles til.
.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;
}
Inline CSS eller ekstern fil?
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å 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.
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.
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.
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.
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.
Inline kun den nødvendige CSS kode
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.
Min forside er rimelig simpel. I mit tilfælde er det bare en liste af mine blogindlæg med titel, dato og antal kommentarer.
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.
Der er altså en masse CSS kode som er helt overflødig at loade på forsiden og vice versa.
Når jeg bruger Sass til at samle de enkelte .sass filer til en færdig CSS fil laver jeg derfor to filer:
- En til forsiden og kategorisider, hvor der blot vises en liste af indlæg.
- En til
single.php
som viser hele indlægget.
De to filer har alt den generelle styling til fælles, som jeg har brudt ud i logiske moduler.
// 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
Ovenstående CSS kode inkluderes i begge filer og derudover inkluderer jeg så den kode som er relevant for hhv. forsiden og blogindlægget.
Jeg har i alt 19 KB CSS kode.
- 41% er Normalize, som jeg måske skal overveje om jeg kan undvære.
- 38% er specifikt til blogindlæg som jeg derved ikke behøver at loade på forsiden.
- 18% er den globale CSS til header og footer.
- 3% er til forsiden, der som sagt er meget simpel.
I WordPress inkluderer jeg de to CSS filer så de ligger inline, baseret på et check for om siden er single.php
eller andre sider.
<style>
<?php if ( is_single() || is_page() ) {
include("css/single.css");
} else {
include("css/homepage.css");
}
?>
</style>
CSS reduceret med 45%
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.
Væk med !important
Når jeg skriver !important i min CSS kode er det et tegn på at jeg har malet mig op i et hjørne.
Det er en sidste udvej. Og det kommer til at bide mig i røven senere hen.
Det gamle design brugte !important 35 gange.
Derfor har jeg fokuseret på at få gennemtænkt mine CSS selectors så jeg undgår at bruge !important i det nye design.
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.
Et eksempel er at jeg styler min font på html
elementet og derefter nedarves det bare til resten af sitet, så jeg ikke skal style min font igen – lige bortset fra input
elementer som ikke nedarver styling og derfor skal styling skrives specifikt på dem.
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
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.
Det giver også browseren mere arbejde med at finde ud af hvad der skal overskrive noget andet.
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.
SVG til grafik
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.
Jeg bruger det fx til den lille graf i mit logo.
Der er forskellige måder at indsætte et SVG billede på og jeg lytter til Chris Coyiers enorme erfaring om SVG (han har skrevet en bog om det: Practical SVG). Hans anbefaling er blot at inline SVG koden direkte i HTML’en. Det har han skrevet om her: A Pretty Good SVG Icon System
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.
Indholdet af SVG filen indsætter jeg i header.php
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.
<a href="https://www.jacobworsoe.dk/" title="jacobworsoe.dk" rel="home" class="blog-title">
<span class="logo"><?php include("svg/logo.svg"); ?></span>
<span class="title"><?php bloginfo( 'name' ); ?></span>
</a>
WordPress
Væk med unødvendige plugins
Jeg har fået fjernet en del plugins, så det bliver mere simpelt, fjerner mulige sikkerhedshuller og gør sitet hurtigere.
Tabeller
TablePress 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.
Syntax highlighting
Jeg brugte Code Prettify til syntax highlighting af kode. Jeg er skiftet til at bruge Prism.js 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 :)
Jeg kan i øvrigt anbefale denne artikel med alle de forskellige muligheder for at skrive og vise kode i WordPress.
Relaterede indlæg
Jeg har brugt Yet Another Related Posts Plugin til at vise relaterede indlæg i bunden af hvert blogindlæg.
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.
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.
Loadtid og konvertering
Okay, det var en lang teknisk snak. Nu skal vi se om det har givet de ønskede resultater.
Google Pagespeed Score hævet fra 86 til 99
Det gamle design havde en pagespeed score på 86 for desktop. Den er nu 99.
Den vigtige metric er dog mobile nu og den er 96.
Er sitet så blevet hurtigere?
Ja, det er det. I gennemsnit er loadtiden blevet forbedret 29%.
Men gennemsnit kan snyde meget og skjule sandheden.
Ikke alle sider loader lige hurtigt.
Jeg har blogindlæg på mere end 6000 ord med masser af billeder. Jeg har meget populære blogindlæg som står for 70% af sidevisningerne som kun har få, men til gengæld meget store billeder. Og så er der forsiden som stort set kun er tekst.
Især det faktum at de har meget forskellige antal sidevisninger gør at de fylder meget forskelligt i gennemsnittet.
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.
22% er et mere retvisende gennemsnit for udviklingen i loadtid.
Hvad med konvertering?
Jeg har tidligere skrevet om hvordan jeg bruger Enhanced Ecommerce til at tracke om brugerne læser mine blogindlæg.
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?
For hvert blogindlæg har jeg en Buy-to-detail Rate, som er forholdet mellem antal sidevisninger og antal læsninger.
På trods af at loadtiden er markant forbedret for alle blogindlæg, så er konverteringen desværre ikke steget – tværtimod.
Jeg tror den store årsag til den lavere konvertering skyldes designet. Jeg har skruet op for font-size
fra 17px
til 20px
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?
Bounce Rate er ligeledes uændret, på trods af den hurtigere loadtid.
Så det nye design har ikke haft den ønskede effekt på konverteringen. Det må jeg gøre bedre i næste design.
Øge sidevisninger pr. besøg
Jeg har læst Don’t Make Me Think mange gange og den kan anbefales til alle der arbejder med noget digitalt.
Her er en god pointe fra bogen omkring navigation.
Som tidligere vist, så har sitet en Bounce Rate på 85% og der bliver kun set 1,17 sider pr. session.
Jeg vil gerne at brugerne fortsætter rundt på sitet og ser nogle flere blogindlæg.
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.
Jeg tracker både hvor mange der åbner burger menuen og hvor mange der klikker i den.
- 1,6% af alle besøg åbner menuen.
- 0,4% af alle besøg klikker på noget i menuen.
Her er de kategorier der bliver klikket på.
Umiddelbart en ret lav konverteringsrate og sider pr. besøg er dog også uændret.
Så selvom Steve Krug har ret i mange ting, så virker en burger menu altså ikke på dette site. Jeg må i tænkeboks.
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.
Lykkedes målene?
Lad os se.
- Mere moderne workflow til udvikling af websitet – Tjek!
- Oprydning i kode og sletning af unødvendige ting – Tjek!
- Hurtigere loadtid – Tjek!
- Højere konvertering – Nope!
- Øge sidevisninger pr. besøg – Nope!
2 kommentarer
Spændende indsigter, Jacob.
Ift. content-tracking, hvordan har du så sat det op med EE? ER det Simo’s metode (https://www.simoahava.com/analytics/track-content-enhanced-ecommerce/), din egen, eller en tredje?
m.
Mange tak Michael!
Content tracking er inspireret af Simo og så har jeg tilføjet nogle ekstra detaljer. Jeg har for nyligt skrevet et alt alt for langt blogindlæg om det i alle detaljer: https://www.jacobworsoe.dk/indhold-enhanced-ecommerce/