The RikVerse rebuild: Fun with Cookies!

tl;dr: Rik learns how to handle cookie consents in a legal and (hopefully) more fun way

This is the fifth in a series of blog posts detailing my journey to rebuild my poetry website - The RikVerse - using Svelte.js, Page.js and Tailwind CSS. Other posts in this series:

  1. Introduction to the project
  2. Setting up the shiny
  3. The client is not the server - 404 errors
  4. Building the blog reader
  5. Fun with Cookies!
  6. The book store bits
  7. Sharing is Caring - social media share buttons
  8. … And finally: the poems!

The code developed in this blog post can be found in this branch on GitHub.

Where were we?

In the previous post I added a blog reader to the site, with a page for listing posts and another page to display each post.

Here’s a quick reminder of my fourteen(!) goals for this site rebuild:

  • [This post] Keep the donate button; make it More Fun To Use
  • [This post] Add in cookie consent (meeting UK ICO guidance for explicit opt-in)
  • [Next post] Make each publication page that book’s keystone page
  • [Next post] Let people read the books in-site
  • [NEW] Add social media share buttons to each page
  • [Future post] Index of all the poems available to read
  • [Future post] Tag filtering functionality on the poems index page
  • [Future post] Easy for the user to access a poem’s associated media files (images, audio, video)
  • [Future post] The landing page should display a randomly selected poem
  • [Done] Simpler, more minimal design; better fonts
  • [Done] Navigation needs to be massively simplified
  • [Done] Avoid 404 Page Not Found errors at all costs!
  • [Done] Add in poetry-and-writing-related blog posts
  • [Done] Get rid of the database!

You know every time you visit a new website and it throws up a “Cookie Consent” banner thing and you have to click the “Accept” button to make it go away - and then you never see it again?

The people who run sites that use generic cookie consent banners are probably breaking the law (at least in the UK). Which isn’t a problem for us users of those sites. But it could well be a problem for those site owners. Because if they break the law, they could get fined. Break the law badly and the fines could be huge!

My take on the UK’s Information Commissioner’s Office’s updated guidance is:

  • Users, when visiting a website, need to explicitly agree to have 3rd Party cookies placed on their device before the cookies are activated
  • The use of of the cookies by each of the 3rd Parties has to be clearly explained
  • We cannot pre-judge the user’s decisions - no more pop-up banners with huge “I ACCEPT” buttons!
  • The user should be able to change their choice about cookies at any time, in an accessible and simple manner

What does this all mean for the RikVerse website?

Just because my website is small, it doesn’t mean that I can ignore these rules. The RikVerse runs best when users allow some 3rd party cookies to be installed on their devices. But I can’t just add them when visitors first arrive at the site, then hope nobody notices … or cares!

Why not? Because this site caters to Poets and Poetasters. These people really do care about stuff like privacy and security. And they know how to organise an online lynch mob!

… Never trust someone who shows an interest in poetry. They’re bad people. Bad!

On the positive side of things, these new cookie requirements give me an opportunity to experiment and learn. If I can make cookie consents on the RikVerse site “Fun”, then everyone wins, yes?

There’s three particular pieces of functionality that I want to run on the new site - each of which requires 3rd Party cookies to work properly:

  • The donation button works with the help of PayPal, thus requires PayPal cookies;
  • Sharing a page to Facebook needs a Facebook share button which only works with their various cookies;
  • Similarly, the Twitter tweet button comes with its own host of Twitter cookies;

… That’s a lot of cookies for a little website!

I’ve also decided to add one further constraint: no popup cookies banner!

The visitor to the RikVerse site will not encounter anything cookie-related until they click on a button which would normally trigger a social media share, video watching event, or donation opportunity. Only at that point will the user be redirected to the Cookie Consents page to make their choices known.

Reading poems, or downloading books, requires no cookies whatsoever. Except the sort you dunk in your beverage of choice, of course!

Anyways. For the proof-of-concept development, I decided to first tackle my need to add a working PayPal donate button to the site.

Svelte store functionality

In order to know, and remember, user choices when it comes to cookie consents, we need to give the RikVerse website some state!

Svelte uses objects it calls stores for keeping track of state across a site. They are quite easy to use, and very responsive when they get to work. Because the RikVerse site will need to keep track of user cookie consents in a number of different components, it made sense for me to hold the state of consent decisions in them.

I created a new file - ./src/handleCookies.js - for building and sharing the stores. Here’s the code that implements the PayPal store:

1
2
3
4
5
6
7
import { writable } from 'svelte/store';

let paypalCookies = writable('');

export {
paypalCookies,
}

The Cookies page is where the user will be making decisions about consents. Here’s the new code for ./src/pages/CookieConsents.svelte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<style>
[ ... stuff ... ]
</style>

<script>
import pageData from '../data/pageData.mjs';

let pageMetadata = pageData.filter(item => item.id === 'cookies')[0];

import { paypalCookies } from '../handleCookies.js';

// PayPal
const paypal_NO_action = () => paypalCookies.set('no');
const paypal_YES_action = () => paypalCookies.set('yes');

</script>

<svelte:head>
<title>{pageMetadata.tabTitle}</title>
</svelte:head>

<h1>Cookie consents on the RikVerse site</h1>

<div>

<!-- PayPal donation cookies -->
{#if $paypalCookies === 'no'}
<button class="choice-button-no">
PayPal cookies blocked
</button>
{:else}
<button class="choice-button" on:click={paypal_NO_action}>
Rik can starve for all I care!
</button>
{/if}

{#if $paypalCookies === 'yes'}
<button class="choice-button-yes">
PayPal cookies allowed
</button>
{:else}
<button class="choice-button" on:click={paypal_YES_action}>
Yes I would like the opportunity to give Rik some money
</button>
{/if}

</div>

Note that after we import { paypalCookies } we can directly access the store’s value by using a $ prefix in front of it: #if $paypalCookies === 'no', etc.

The Footer bar is also going to be affected by the PayPal cookie consent. Specifically, we will amend the begging button’s action dependant on whether they allow cookies, or refuse them, orr haven’t yet made a decision. This is the updated code for ./src/component/Footer.svelte:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<style>
[ ... stuff ... ]
</style>

<script>
import NavigationLinks from './NavigationLinks.svelte';

import { navigateTo } from '../utilities.js';
import { paypalCookies } from '../handleCookies.js';

const setupPayPalAction = (e) => navigateTo('/cookies');
</script>

<footer>
<NavigationLinks />

{#if $paypalCookies === 'yes'}
<p>USER LIKES COOKIES! PayPal functionality will go here!!</p>
{:else if $paypalCookies === 'no'}
<p>User does not want any cookies - PayPal functionality will be blocked</p>
{:else}
<button on:click={setupPayPalAction}>
<img
src="/images/donate-button.png"
alt="Image button for PayPal donations to support the RikVerse website" />
</button>
{/if}

[ ... stuff ... ]
</footer>

Testing the code shows that it is working - both the Cookies page and the Footer component react appropriately to user choices about PayPal cookies.

This is the view on first load:

User declines cookies:

User accepts cookies:

… If only it was all this easy!

Sadly, it’s not that easy!

As a test, I clicked the reload button on my browser … and saw this:

The problem is, we have no longer-term memory of user decisions.

Luckily all modern browsers come equipped with functionality which makes this an easy thing for us to remedy:

  • window.sessionStorage - for single visits to a site; and
  • window.localStorage - for remembering stuff on a long-term basis, between visits to a site.

I added code to ./src/handleCookies which allowed me to do exactly this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// localStorage and sessionStorage functionality
const isMultiSessionStorageSet = () => {

if (localStore.getItem('rememberChoicesBetweenSessions') === 'yes') return true;
return false;
}

let localStore = window.localStorage,
sessionStore = window.sessionStorage,
store = (isMultiSessionStorageSet()) ? localStore : sessionStore;

const setToStore = (key, choice) => store.setItem(key, choice);
const getFromStore = (key) => store.getItem(key);

// transfer data between session and local stores
const useLocalStore = (flag) => {

let storedItems = [];

let populateItemsArray = (s) => {

let l = s.length;

for (let i = 0; i < l; i++) {

storedItems.push(s.key(i));
}
}

if (flag) {

// moving data from sessionStore to localStore
populateItemsArray(sessionStore);

storedItems.forEach(key => localStore.setItem(key, sessionStore.getItem(key)));
localStore.setItem('rememberChoicesBetweenSessions', 'yes');
sessionStore.clear();
store = localStore;
}
else {

// moving data from localStore to sessionStore
populateItemsArray(localStore);

storedItems.forEach(key => sessionStore.setItem(key, localStore.getItem(key)));
sessionStore.removeItem('rememberChoicesBetweenSessions');
localStore.clear();
store = sessionStore;
}
};

Now all I had to do was get the Svelte store to update the browser stores at the same time as it updated itself. Svelte allows us to do this through functionality which it calls custom stores.

I added some more code to ./src/handleCookies to get this working:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Svelte store functionality
import { writable } from 'svelte/store';

function createCookieJar (id) {

const cookieType = id;

const { subscribe, set } = writable(getFromStore(cookieType));

return {
subscribe,
set,
setTo: (val) => {

set(val);
setToStore(cookieType, val);
}
};
}

let paypalCookies = createCookieJar('paypal');

export {
paypalCookies,
}

… and updated my button handler functions in ./src/pages/CookieConsents.svelte:

1
2
3
4
5
6
7
8
9
10
11
12
<script>
import pageData from '../data/pageData.mjs';

let pageMetadata = pageData.filter(item => item.id === 'cookies')[0];

import { paypalCookies } from '../handleCookies.js';

// PayPal
const paypal_NO_action = () => paypalCookies.setTo('no');
const paypal_YES_action = () => paypalCookies.setTo('yes');

</script>

… And, with fingers crossed, tested a browser reload:

(Happy dance!)

Accepting and/or rejecting all cookies at once

Cookie banners have been criticised because they spring up as soon as a new visitor arrives on a site, with a big “I AGREE” button and a tiny “I want to choose” link which leads to almost everybody clicking on the “I AGREE” button to make the banner go away, never to be seen again.

While the RikVerse website won’t be working like that - as I said earlier, users only get directed to the Cookies page when they try to access some functionality that needs cookie consent before it works - it would be nice if the users could have big “I AGREE” (and equally big “NO COOKIES”) buttons on the Cookies page.

I went away and coded up this functionality. I also added in the buttons and cookie handlers for Twitter and Facebook. I won’t bore you with the code (it’s in GitHub if you want to view it), but here’s a screen grab of the end result:

Making PayPal work (when the user says: yes!)

This post has already gone on too long - and I haven’t generated any donations yet!

That’s because we have one final task to complete - adding PayPal functionality to the Footer component so that when people click on the button they get directed to PayPal to make their donation.

Making a PayPal donate button is a blog post in itself. Luckily those nice people over at Hands On Fundraising have already written it, so I don’t need to bother.

The upshot of it all is that we have to visit the PayPal site and create a donate button. At the end of the process the site spits out some html code which we copy-and-paste into our code.

PayPal will only try to add RikVerse-related cookies to the user’s browser or device when that code - which is actually a html form element - renders. This makes it really easy to control … with the help of Svelte, of course! Only if the user has actively opted in to PayPal cookies will the PayPal form be included in the rendered page.

Here’s the final code for the ./src/components/Footer.svelte component:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
<style>
footer {
@apply bg-blue-800 py-4;
}
p {
@apply text-sm text-center text-gray-500 m-0 mt-4 pt-4;
border-top: 1px solid rgba(200, 200, 200, 0.3);
}
button {
@apply bg-blue-800 block w-full mt-5 border-0 outline-none;
}
button img, input[type="image"] {
@apply block mx-auto border-0 outline-none;
height: 40px;
}
form {
@apply pt-6;
}
a {
@apply text-gray-200 no-underline;
transition: color 0.5s;
}
a:hover {
@apply underline;
color: #fffc00;
}

@media (min-width: 768px) {
footer {
@apply rounded-lg mb-4;
}
}
</style>

<script>
import NavigationLinks from './NavigationLinks.svelte';

import { navigateTo } from '../utilities.js';
import { paypalCookies } from '../handleCookies.js';

const setupPayPalAction = (e) => navigateTo('/cookies');

const myPayPalAction = (e) => window.location = 'https://www.paypal.me/RichardRoots?locale.x=en_GB';
</script>

<footer>
<NavigationLinks />

{#if $paypalCookies === 'yes'}
<form
action="https://www.paypal.com/cgi-bin/webscr"
method="post"
target="_top">

<input type="hidden" name="cmd" value="_donations" />
<input type="hidden" name="business" value="*****" />

<input
type="hidden"
name="item_name"
value="To keep the RikVerse website up-and-running" />

<input type="hidden" name="currency_code" value="GBP" />

<input
type="image"
src="http://rikverse2020.rikweb.org.uk/images/donate-button.png"
border="0"
name="submit"
title="PayPal - The safer, easier way to pay online!"
alt="Donate with PayPal button" />

<img
alt=""
border="0"
src="https://www.paypal.com/en_GB/i/scr/pixel.gif"
width="1"
height="1" />
</form>
{:else if $paypalCookies === 'no'}
<button on:click={myPayPalAction}>
<img
src="/images/donate-button.png"
alt="Visit Rik's PayPal.me page to give him a donation" />
</button>
{:else}
<button on:click={setupPayPalAction}>
<img
src="/images/donate-button.png"
alt="You need to decide whether to accept/refuse
cookies before you can give Rik a donation" />
</button>
{/if}

<p>
&copy;2020 Rik Roots. Site built and maintained by RikWorks.<br />
Tech: <a href="https://svelte.dev/">Svelte</a> scaffold,
<a href="https://visionmedia.github.io/page.js/">Page.js</a> routing,
<a href="https://tailwindcss.com/">Tailwind</a> css
</p>
</footer>

Okay … I’ve had too much fun with cookies today, so for my next post I’ll implement the functionality needed to list my books, and to read my books without leaving the RikVerse.

And in the post after that I shall return to cookies - specifically to add social media sharing buttons to every page on the site.