The RikVerse rebuild: Building the blog reader

tl;dr: Rik adds a blog post reader to the new RikVerse website

This is the fourth 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 blogpost can be found here on GitHub.

Where were we?

In the previous post I investigated and fixed a major issue which was causing “404 Page Not Found” errors when users attempted to refresh a page on the new RikVerse site. Now that that issue has been resolved, we can move on to more fun stuff!

Here’s a quick reminder of my thirteen goals for this site rebuild:

  • [This post] Add in poetry-and-writing-related blog posts
  • [This post] Get rid of the database!
  • [Next post] Keep the donate button; make it More Fun To Use
  • [Next post] Add in cookie consent (meeting UK ICO guidance for explicit opt-in)
  • [Future post] Make each publication page that book’s keystone page
  • [Future post] Let people read the books in-site
  • [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!

Question: what is a “blog reader”?

A traditional blog site, or blogging platform, comes in two parts:

  • the “writer” part, usually some form of console where users can log in to compose and publish blog posts; and

  • the “reader” part, where everyone else can see a list of blog posts, and read individual articles.

The RikVerse is, primarily, a site where I can showcase my books and poems. It is not a blogging platform. However I have, over the years, offered opinions in various venues on the state of poetry, writing and publishing. As part of this website rebuild I wanted to gather those witterings together in one place so that complete strangers can discover them for their own entertainment.

I also have an aim for this rebuild to rid myself of a database on the back end. The site’s content is (mostly) static: it only gets updated when I feel an urge to write a poem, or publish a book, or offer an opinion on something related to the terrible state of English literature.

Thus my thinking is: I can store the poems, book details and blog posts in .js and .html files, which can be fetched by the front end whenever a page or person has a need for them.

… Yes, it’s a simplistic architecture. But it fits the requirements this particular website. It is enough to meet the client’s (in other words, my) wishes!

Finalizing the design

At the moment, the site does not look pretty. We need to change this. But before we get onto coding up the header and footer components I need to correct a small bug.

One of the screen responsiveness things I’m doing is increasing the base font size as we move through the breakpoints. I was doing this by setting font sizes in px on the body element for each @media block (this is all happening in ./src/App.svelte).

Tailwind CSS very sensibly does its font sizing magic in rem units. These units are relative to the font size of the entire document - which needs to be set on the html element.

… All fixed now!

Finalizing the Navigation.svelte component

My Navigation component acts both as the site’s banner, and as the main site navigation - which will be a horizontal line of anchor links. I didn’t want to go overboard on designing this component, but I did want it to stand out.

I made a decision to rescue the “RikVerse” banner image from the old site. This - alongside the .ico image that goes in the browser’s tab - was the only old site furniture I planned to reuse.

For the navigation links, I decided to go super-simple.

I know every modern site in the world likes to collapse its main navigation into a ‘hamburger’ link for small screens. I also know that hiding the navigation bar (with its ‘home’ and ‘hamburger’ icons) when the user scrolls down the screen, but showing it fixed at the top of the screen when the user scrolls back up, remains “all the rage” in UX circles.

I made some design decisions:

  • No icons in the navigation - links to be text-only.
  • No slippy-slidey collapsing navigation bar which only shows up on mobile screens.
  • Navigation links to be duplicated in both the Navigation and the Footer components.

… I think this will work well for the RikVerse site, where I need to keep distractions to a minimum. The user has arrived at the site to read poems, not to be pulled away from the words by animated shiny!

Here is the complete code for ./src/components/Navigation.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
<script>
import NavigationLinks from './NavigationLinks.svelte';
</script>

<style>
nav {
@apply pt-2 pb-2;
background: rgb(2,0,36);
background: linear-gradient(0deg, rgba(2,0,36,1) 0%, rgba(9,9,121,1) 5%, rgba(173,184,236,1) 38%, rgba(185,234,244,1) 100%);
}

div {
width: 100%;
}
a {
@apply w-full;
}
img {
margin: 0 auto 1rem auto;
width: 40%;
}

@media (min-width: 768px) {

nav {
@apply rounded-lg;
}
}
</style>

<nav>
<div title="Click here to see a random poem">
<a href="/">
<img src="/images/rikverse-logo.png" alt="RikVerse banner logo" />
</a>
</div>

<NavigationLinks />
</nav>

And here’s the code for ./src/components/NavigationLinks.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
<script></script>

<style>
div {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
grid-gap: 0 0;
justify-items: center;
}
a {
@apply inline-block text-center m-0 text-gray-200 no-underline;
transition: color 0.5s;
}
a:hover {
@apply underline;
color: #fffc00;
}
</style>

<div>
<a href="/index">Poems</a>
<a href="/publications">Books</a>
<a href="/blog">Blog</a>
<a href="/about">About Rik</a>
<a href="/cookies">Cookies</a>
</div>

How many CSS classes can you see in that code?

This is where the magic of combining Svelte with Tailwind CSS really shines!

Tailwind makes it super-easy to apply classes to elements through its @apply function. Where Tailwind is lacking a class for something - such as for styling a grid - we can add in that styling using normal CSS.

And then Svelte goes away and does its magic to make sure that the styles applied to the elements in a component remain local to that component! The styling that the Navigation component adds to its div and a elements is completely separated from the styling applied by the NavigationLinks component to its own equivalent elements.

Also, adding a component to another component is really easy in Svelte - import the component in the script element (note: paths are relative to the receiving component), then use it wherever needed in the layout.

The Footer component is a little more interesting because it includes a Button element:

The idea is that if people want to give me some £money, they can click on that button and make a donation via PayPal. However to do this, users need to agree to allow PayPal to add cookies to the site. Only after they agree will they be able to make a donation.

… Yeah. It’s a palaver. But it’s also the law. I’ll be describing how I handle cookie consents in the next post. For now, clicking on the button just takes the user to the Cookie Consents page.

Here’s the entire code for ./src/components/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
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
<script>
import NavigationLinks from './NavigationLinks.svelte';

const setupPayPalAction = (e) => {

// Page.js only watches for anchor link clicks.
// - we need to create and trigger a click event on an anchor element
// to make sure the required navigation happens
let a = document.createElement('a');
a.href = '/cookies';

document.body.appendChild(a);
a.click();
a.remove();
};
</script>

<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 {
@apply block mx-auto border-0 outline-none;
height: 40px;
}
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>

<footer>
<NavigationLinks />

<button on:click={setupPayPalAction}>
<img
src="/images/donate-button.png"
alt="Image button for PayPal donations to support the RikVerse website" />
</button>

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

The button element is not an anchor - it doesn’t have an href= attribute. Instead, we have to let Svelte know what to do when a user clicks on it. We do this through the on:click={setupPayPalAction} attribute - see the Svelte documentation for more information on handling events in Svelte components.

Building the RikVerse Blog reader

A blog post consists (for me, at least) of two parts:

  • The bit you read - the “copy”, and

  • The various bits of data surrounding it such as title, publication date, tags, etc - the “meta”

Normally all of this stuff gets added to a database as a single record.

For the RikVerse, I wanted to display a list of blog posts which, when clicked on, open up the selected post in a different display. Nothing surprising here.

Again, normal procedure is to send a request to the backend to get all the data required to display the list (from each record, for example: title, summary, publication date, link to article, etc). Then to read a particular post, another request gets sent to the backend to get all the data contained in the record for that post.

It’s a sensible way to do these things.

But it does mean that some date (title, summary, publication date, etc) gets sent over the network twice. Even in these days of super-fast broadband and 5G wireless, it feels a bit wasteful to me.

Also, I’m trying to break free from paying for a database on the back end. Because: nothing in this world is truly free.

So I decided to split the blog post data. For each post:

  • The “copy” part of the post is saved in its own file (in plain html) in the ./public/posts/ directory - which makes sense as this comprises the vast bulk of the post and should only be sent over the wires when required; while

  • The “meta” part of the post (everything that is not “copy”) gets stored as an object in an array in a different file - ./src/data/blogpostData.mjs

Stay with me here … I do have excellent reasons for doing it this way!

This is a snippet of the blogpostData.mjs file. As you can see, the object attributes are (pretty much) the same as the ones used in the pageData.mjs file.

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
const blogpostData = [
{
id: "copyrights",
title: "[sticky] The RikVerse Creative Commons licences",
tabTitle: "RikVerse blog post",
description: "... Using Rik's poems in your own creative work, for free.",
publishdate: "Today",
imageUrl: '/images/RV-blog_share.png',
imageText: 'Image advertising the RikVerse Blog page',
},
{
[... blog post metadata ...]
},
{
id: "giving-books-away-for-free",
title: "Why do you give your books away for free, Rik?",
tabTitle: "RikVerse blog post",
description: "... Seriously, why do that?",
publishdate: "2014-12-02",
imageUrl: '/images/RV-blog_share.png',
imageText: 'Image advertising the RikVerse Blog page',
},
{
[... another blog post ...]
},
];

export default blogpostData;

This is all the information we need to start constructing the blog posts listing page. And, because the file is a Javascript module, we can use it in the same was as we use the pageData module.

The blog post listings page

Here’s the entire code for the ./src/pages/Blog.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
<script>
import pageData from '../data/pageData.mjs';
import blogpostData from '../data/blogpostData.mjs';

import BlogListing from '../components/BlogListing.svelte';

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

// We store dates in the format 'YYYY-MM-DD'
// - this gives us today's date in the same format
let date = new Date();
date = date.toISOString().split('T')[0];

// "Sticky" posts go at the top of the listing
let topPosts = blogpostData.filter(item => item.publishdate.indexOf('Today') >= 0);

// All other posts, excluding future-dated ones
let posts = blogpostData.filter(item => item.publishdate <= date);

// Sort post groups by differing criteria
topPosts.sort((a, b) => b.title < a.title ? 1 : -1);
posts.sort((a, b) => b.publishdate > a.publishdate ? 1 : -1);
</script>

<style>
h1 {
@apply mb-2
}
p {
@apply border-b pb-4 mb-0;
}
</style>

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

<h1>The RikVerse Blog</h1>

<p>
Where Rik contemplates stuff about poetry, writing, publishing,
and occasional dragon sightings.
</p>

{#each topPosts as listing}
<div>
<BlogListing {listing} />
</div>
{/each}

{#each posts as listing}
<div>
<BlogListing {listing} />
</div>
{/each}

The interesting thing here is the Svelte markup - #each … - which we use to iterate through the two arrays we created. We pass this data onto a new component called BlogListing; this is the component which does all the work of laying out the data in a nice way on the page.

This is the ./src/component/BlogListing.svelte code:

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
<script>
import { prettifyDate } from '../utilities.js';

// Svelte uses 'export' to identify attributes a component can import and use
// - in this case, we're expecting to import a Javascript object
export let listing;
</script>

<style>
div {
@apply pl-0;
transition: padding-left 0.5s;
}
div:hover {
@apply pl-2;
}
a {
@apply text-gray-900 no-underline;
transition: color 0.5s;
}
a:hover {
@apply text-green-700 no-underline pl-12;
}
h3 {
@apply mb-0;
}
summary {
@apply italic whitespace-pre-wrap m-0 mb-4;
}
time {
@apply block text-sm whitespace-pre-wrap m-0;
}
</style>

<div>
<a href="/blog/{listing.id}">
<h3>{listing.title}</h3>
<time datetime="{listing.publishdate}">
{prettifyDate(listing.publishdate)}
</time>
<summary>{listing.description}</summary>
</a>
</div>

There’s nothing remarkable in this code - except for the wierd export let listing; line. As ever, the Svelte documentation does a far better job than I can to explain how it passes data between components using attributes and properties.

The anchor link <a href="/blog/{listing.id}"> - this is a new route that we need to add to our router code. I updated the ./src/routes.js file as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const routes = [
{
// The Author page - because "Every ass loves to hear himself bray"
path: '/about',
component: About
}, {
// Blog post page
path: '/blog/:slug',
component: BlogPost
}, {
// Index of links to individual blog posts
path: '/blog',
component: Blog
}, {
[... etc ...]
}
];

Now we’re in Page.js territory. The client-side router’s documentation is less than useful if you don’t know what you’re looking for. However Jack Whiting’s blog post, which inspired this rebuild exercise, details how we can access that mysterious :slug value (which will be the blog post’s id string) in the component that Page routes us to.

The blog article display page

So the user has wandered into the RikVerse, discovered the blog listings and clicked on one of the links. Now we have to display that article to them.

Page.js tells Svelte to load up the BlogPost page. The code for this Svelte component is located in ./src/pages/BlogPost.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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<script>
import blogpostData from '../data/blogpostData.mjs';

import {
prettifyDate,
scrollToTopOnLoad,
navigateTo } from '../utilities.js';

// This line lets us access the :slug value supplied by Page.js
export let params;

let post = '';

let postData = blogpostData.filter(item => item.id === params.slug);

// Sanity check - does the post exist?
if (postData.length) {

postData = postData[0];

// Fetch the blog post's content (stored on the server in an html file)
fetch(`/posts/${params.slug}.html`)
.then(res => res.text())
.then(res => {

post = res;

scrollToTopOnLoad();
})
.catch(error => console.log(error.message));
}
else navigateTo('/error');

</script>

<style>
summary {
@apply text-blue-700 italic mb-2;
}
time {
@apply block text-sm m-0 mb-4 pb-4 border-b;
}
</style>

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

<!--
Only process when we have a post
- testing on publishdate because prettifyDate(postData.publishdate)
will error if the value is not a string
-->
{#if postData.publishdate}
<h1>{postData.title}</h1>

<summary>{postData.description}</summary>

<time datetime="{postData.publishdate}">
Published: {prettifyDate(postData.publishdate)}
</time>

<article>
<!--
This is dangerous!
Never output raw html markup from an untrusted source!
-->
{@html post}
</article>
{/if}

The ./src/utilities.js file is a small collection of functions which I can import and use in various places across the code base:

  • prettifyDate() makes a YYYY-MM-DD date string more readable - I could have used the moment.js library (which is fantastic!) but I only need a couple of date conversion functions on this site so I’m saving some bandwidth by coding up the functions myself.

  • scrollToTopOnLoad() triggers a browser scroll action to the top of the page.

  • navigateTo() - I refactored the Footer’s button redirection code into the utilities file so I could use it in other components, like here.

The Svelte commands #if ... /if and @html ... are properly explained in the Svelte documentation.

… And that is pretty much it!

There’s just one last thing to do before I wrap up this post …

Danger! Danger! 404 Page Not Found!

As it stands, every blog post page will error if the user clicks on their browser’s refresh button. We need to generate some static redirect pages for all the post pages.

I updated the ./pageBuilder.mjs file to automagically do this work for us:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Process the router base pages index files
import pageData from './src/data/pageData.mjs';
pageData.forEach(page => {

checkDirectory(`./public/${page.id}`)
.then(res => writeIndexFile(page, `./public/${page.id}`))
.then(res => console.log(res))
.catch(err => console.log(err));
});

// Process the blogpost files
import blogpostData from './src/data/blogpostData.mjs';
blogpostData.forEach(post => {

checkDirectory(`./public/blog/${post.id}`)
.then(res => writeIndexFile(post, `./public/blog/${post.id}`))
.then(res => console.log(res))
.catch(err => console.log(err));
});

… And that really is is for this post. In the next post I shall be having Fun With Cookies!