Setup and deploy new Hexo blog

tl;dr: In which Rik experiments with Hexo - learning how to start a new blog project with it, tweak the default theme, add Disqus to the mix for the comment-shiny, build the site locally, and deploy that build to an Amazon Web Services S3 Bucket.

Purpose

I’m on a bit of a learning binge at the moment and, for once, I want to document that learning as I go along. Thus the need for something where I can post my discoveries as I make them.

I wasn’t planning to build a new micro-blogging site. My original intention was to use a third party service. Things I looked at included:

  • GitHub Pages - but Jekyll annoyed me.
  • Google Cloud - simple enough, but I want to learn more about AWS st the moment, and going for the Wordpress option is overkill for my needs.
  • Ghost - it looks gorgeous, and I’ve been wanting to play with it for ages, but for this exercise I don’t need the full admin console experience. Maybe next time.

I found Hexo by accident, going through a list of static site generators. The online documentation looked to be adequate and the setup seemed easy enough. There’s no console (out of the box) for preparing pages and posts; the creative writing happens in YAML files. It comes with a busy plugin page and some gorgeous themes.

I had to give this new shiny a closer inspection.

One thing I noticed was that a lot of the activity around Hexo seems to be coming from the Eastern Asia part of the world, which did impact on my research - my Chinese reading skills are seriously buggy; doing the copy/paste thing into Google Translator gets boring very quickly. But that is certainly no show-stopper: Hexo has a gentle enough learning curve to get it stood up and operational in no more than a couple of hours.

Hello Hexo, my new friend.

Hexo runs in NPM; the download is a set of cli commands which you use to create, build and manage your blog projects.

1
2
3
4
5
6
7
8
9
10
$ npm install -g hexo-cli
$ hexo init rikblog

# the NPM build ran automatically for me when the git clone failed
# and Hexo decided it was going to 'copy the data' instead
$ cd rikblog
$ npm install

# by default, the server will start on http://localhost:4000
$ hexo server

I was impressed! Everything just … worked. Even more impressive is that you can alter files and refresh the browser to see the changes in near-real life (no-toolchain heaven!)

Don’t forget the git

I’m not planning to host the code for this site on GitHub; doesn’t mean I can ignore the need to git my working directory. I added a .gitignore file to the root directory (see below) and then initialized the git in the normal way - git init.

1
2
3
4
5
6
7
8
9
10
11
12
13
# excluded folders
node_modules
notes
public

# excluded environmentals
*-env.js

# generated stuff I don't want in git repository
.DS_Store
npm-debug.log*
yarn-debug.log*
yarn-error.log*

This theme ain’t good enough …

Hexo’s out-of-the=box functionality looks nifty; it’s theme less so. I did some investigating to see if there were things I could change. Turns out Hexo’s theming capabilities are very nicely modularized, and the CSS itself is very nifty. Under the hood Hexo seems to use Stylus for managing and building the code. Stylus was new to me, but I’m struggling to see much of a difference between it and LESS/SASS … well, my tinkering hasn’t broken anything yet.

I didn’t want to change the default theme’s code, so instead I created my own theme from it:

1
$ cp -R ./themes/landscape ./themes/rikblogtheme

The changes I made to my new theme were minimal. I decided I needed a banner that was a little more, well, me. To change the image I created something suitable (dimensions: 1920 x 1200), called it banner.jpg and overwrote the original image in the /themes/rikblogtheme/source/css/images/ folder.

I also decided to make the base font size a lot bigger, and give links a better color. These values are set in Stylus variables in the appropriately named themes/rikblogtheme/source/css/_variables.styl file:

1
2
3
4
color-link = #800
font-size = 20px
logo-size = 56px
subtitle-size = 24px

Those changes don’t affect the header copy. To amend the header and strapline, and the menu links at the top of the site, I had to dig into the themes/rikblogtheme/source/css/_partial/header.styl file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$logo-text
text-decoration: none
color: #ffb
font-weight: 300
text-shadow: 1px 1px 1px black

$nav-link
float: left
color: #ffb
text-decoration: none
text-shadow: 1px 1px 1px black
transition: color 0.4s
display: block
padding: 20px 15px
&:hover
color: #fff

To see these changes on the site, I needed to update Hexo’s nifty _config.yml file.

Hexo’s nifty _config.yml file

The config file is at the heart of every Hexo-based blog site. It is pleasingly simple, with not too many options to worry about. Don’t misunderstand me: Hexo is a highly flexible blogging platform but, because much of its functionality comes from third-party themes and plugins, the core config file only has to worry about the essential stuff.

Before finalizing the config file I went away and set up an S3 bucket on AWS to host the production blog, alongside associated IAM and Route53 stuff to get the AWS magic to happen - material for a future blog post. The end result was a web address for my blog: http://code-blog.rikworks.co.uk/ - with such a snazzy name it will soon be charging towards the front page of the search engine results.

Rather than explain stuff, I’ll post the (almost complete) config file, with comments along the way:

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
# Hexo Configuration
# Docs: https://hexo.io/docs/configuration.html
# Source: https://github.com/hexojs/hexo/

# Site - this is all basic stuff
# - 'title' and subtitle are the site header and strapline
title: Rik Codes
subtitle: An occasional series of posts on coding stuff
description:
author: Rik Roots
language: en
timezone: Europe/London

# URL - which I pre-created in AWS - it leads to an S3 bucket
url: http://code-blog.rikworks.co.uk
root: /
permalink: :year/:month/:day/:title/
permalink_defaults:

# Directory - all default values here
source_dir: source
public_dir: public
tag_dir: tags
archive_dir: archives
category_dir: categories
code_dir: downloads/code
i18n_dir: :lang
skip_render:

# Writing - the only thing I changed here was 'post_asset_folder'
new_post_name: :title.md # File name of new posts
default_layout: post
titlecase: false
external_link: true
filename_case: 0
render_drafts: false
post_asset_folder: true
relative_link: false
future: true
highlight:
enable: true
line_number: true
auto_detect: false
tab_replace:

# Home page setting - I increased the per_page value to 25
# - blog sites that only show 10 posts annoy me
index_generator:
path: ''
per_page: 25
order_by: -date

# Category & Tag - nothing changed here
default_category: uncategorized
category_map:
tag_map:

# Date / Time format - I got rid of the seconds
date_format: YYYY-MM-DD
time_format: HH:mm

# Pagination - again increased to 25
per_page: 25
pagination_dir: page

# Extensions - this is where I tell Hexo to use my new theme
theme: rikblogtheme

# Deployment - nothing changed here
# - I'll write my own Node solution to deploy to AWS S3
deploy:
type:

Before moving on, I need to detail one gotcha which left me scratching my head for a while. Hexo has a cache. While this was not an issue while I was making changes to stuff locally - the server does admirable work catching those - I couldn’t understand why some changes wouldn’t deploy to AWS. The solution is simple: before building the site, get rid of the cache and public folder by running $ hexo clean on the command line - problem solved!

Rik’s bespoke deployment-to-S3 code

Hexo has a community plugin to perform deployments to AWS S3 - hexo-deployer-aws-s3. I couldn’t get it to work; my .aws/credentials file is too busy for the plugin, and I always forget to set the appropriate environment variables in the CLI before trying to do stuff.

Instead, I wrote my own solution. It involves adding 3 Javascript files to the project root folder and updating the project’s package.json file so I can do the deployment from the command line.

Security warning: this solution involves hard coding my AWS keys into a file. Never commit a file with passwords, keys or any other security-related data to a git repository! When I was writing my .gitignore file (above) I added a line to exclude any file matching the pattern *-env.js which should be enough to keep me from sharing my credentials with the world … but mistakes can, and do, happen.

The following solution will, when supplied with a path to a folder and the name of an S3 bucket, upload the contents of that folder (but not the folder itself) to S3.

The solution uses Amazon’s wonderful and yet terrifyingly complex AWS Software Development Kit, which we first need to add to the code base by running a yarn command: yarn add aws-sdk.

  1. Create a utilities file utilities.js where the bulk of the code will live:
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
// Require statements
const S3 = require('aws-sdk/clients/s3');
const fs = require('fs');

// aws-sdk does all the heavy lifting to gain access to the S3 fortress
const getS3Handle = (credentials) => {
return new Promise((resolve, reject) => {
let h = new S3({
apiVersion: '2006-03-01',
region: 'eu-west-1',
credentials: credentials
});

if(h && h.config && h.config.credentials){
resolve(h);
}
else{
reject(new Error('S3 handle lacks credentials'));
}
});
};

// read a file (asynchronous) - nothing special here
const readFile = (file) => {
return new Promise((resolve, reject) => {

fs.readFile(file, (err, data) => {
if (err) {
reject(err);
};
resolve(data);
});
});
};

// aws-sdk handles transfer of files to the S3 bucket
const pushFileToS3 = (handle, bucket, key, file) => {
return new Promise((resolve, reject) => {

let params = {
Body: file,
Bucket: bucket,
ContentType: getMimetype(key),
Key: key
};

console.log(`Pushing ${key} to S3`);

handle.putObject(params, (err, data) => {
if (err) {
reject(err);
}
resolve('file uploaded');
});
});
};

// most of the work happens here
// - folders are sent for recursive processing
// - files get prepared for upload to S3
const checkForFile = (handle, bucket, folder, path, filename) => {
return new Promise((resolve, reject) => {

fs.stat(`./${folder}/${filename}`, (err, res) => {
if(err){
reject(err);
}
if(res.isFile()){
readFile(`./${folder}/${filename}`)
.then((file) => {
return pushFileToS3(handle, bucket, `${path}${filename}`, file);
})
.then((message) => {
resolve();
})
.catch((err) => {
console.log(err);
reject(err);
});
}
else{
pushFolderContentsToS3(handle, bucket, `${folder}/${filename}`, `${path}${filename}/`)
.then(() => {
resolve();
})
.catch((err) => {
reject(err);
});
}
});
});
};

// reading directories and processing directory contents
const pushFolderContentsToS3 = (handle, bucket, folder, path) => {
return new Promise((resolve, reject) => {

let items, i, iz, params;

path = path || '';

fs.readdir(`./${folder}`, (err, res) => {
items = res;
for(i = 0, iz = items.length; i < iz; i++){
checkForFile(handle, bucket, folder, path, items[i])
.catch((err) => {
console.log(err);
})
}
resolve('reading directory');
});
});
};

// Mimetype - guess from the file .tld (default to 'text/plain')
const mimetypes = {
aac: 'audio/aac',
abw: 'application/x-abiword',
arc: 'application/octet-stream',
avi: 'video/x-msvideo',
azw: 'application/vnd.amazon.ebook',
bin: 'application/octet-stream',
bz: 'application/x-bzip',
bz2: 'application/x-bzip2',
csh: 'application/x-csh',
css: 'text/css',
csv: 'text/csv',
doc: 'application/msword',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
eot: 'application/vnd.ms-fontobject',
epub: 'application/epub+zip',
es: 'application/ecmascript',
gif: 'image/gif',
htm: 'text/html',
html: 'text/html',
ico: 'image/x-icon',
ics: 'text/calendar',
jar: 'application/java-archive',
jpeg: 'image/jpeg',
jpg: 'image/jpeg',
js: 'application/javascript',
json: 'application/json',
mid: 'audio/midi',
midi: 'audio/midi',
mpeg: 'video/mpeg',
mpkg: 'application/vnd.apple.installer+xml',
odp: 'application/vnd.oasis.opendocument.presentation',
ods: 'application/vnd.oasis.opendocument.spreadsheet',
odt: 'application/vnd.oasis.opendocument.text',
oga: 'audio/ogg',
ogv: 'video/ogg',
ogx: 'application/ogg',
otf: 'font/otf',
png: 'image/png',
pdf: 'application/pdf',
ppt: 'application/vnd.ms-powerpoint',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
rar: 'application/x-rar-compressed',
rtf: 'application/rtf',
sh: 'application/x-sh',
svg: 'image/svg+xml',
swf: 'application/x-shockwave-flash',
tar: 'application/x-tar',
tif: 'image/tiff',
tiff: 'image/tiff',
ts: 'application/typescript',
ttf: 'font/ttf',
vsd: 'application/vnd.visio',
wav: 'audio/wav',
weba: 'audio/webm',
webm: 'video/webm',
webp: 'image/webp',
woff: 'font/woff',
woff2: 'font/woff2',
xhtml: 'application/xhtml+xml',
xls: 'application/vnd.ms-excel',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
xml: 'application/xml',
xul: 'application/vnd.mozilla.xul+xml',
zip: 'application/zip',
'3gp': 'video/3gpp',
'3g2': 'video/3gpp2',
'7z': 'application/x-7z-compressed',
};

// helper function to get a file's correct mime type value
const getMimetype = (filename) => {
let test = filename.split(/.+\./),
mime = (Array.isArray(test) && test[1] && mimetypes[test[1]]) ? mimetypes[test[1]] : 'text/plain';
return mime;
};

// only need to export two functions
module.exports = {
getS3Handle: getS3Handle,
pushFolderContentsToS3: pushFolderContentsToS3
};
  1. Create the deployment file deploy.js which node will run:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Require statements
const { credentials } = require('./aws-env.js');
const { getS3Handle, pushFolderContentsToS3 } = require('./utilities.js');


// I'm hard-coding the source and destination values; they won't change
var folderName = 'public';
var bucketName = 'code-blog.rikworks.co.uk';

getS3Handle(credentials)
.then((s3Handle) => {
return pushFolderContentsToS3(s3Handle, bucketName, folderName);
})
.then((message) => {
console.log(message);
})
.catch((error) => {
console.log(error);
});
  1. Create the environment values file aws-env.js - this file must never be committed to a git repository
1
2
3
4
5
6
7
8
const credentials = {
accessKeyId: 'my-access-key-id',
secretAccessKey: 'my-secret-access-key'
};

module.exports = {
credentials: credentials
}

We can test this code by calling it - hexo clean && hexo generate && node deploy.js - to see what happens in the S3 bucket. If all goes well, I will get myself a working blog at http://code-blog.rikworks.co.uk/ - it works for me (and you: you’re reading it!)

Before this (very long) post finishes, I’ll share my package.json file, where I’ve added a couple of scripts to save on typing. It’s not a proper toolchain, but it’s enough for my needs. To trigger the deployment, run yarn run deploy on the command line. Magic!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"name": "rik-codes-blog",
"version": "1.0.0",
"private": true,
"hexo": {
"version": "3.7.1"
},
"scripts": {
"build": "hexo clean && hexo generate && hexo server",
"deploy": "hexo clean && hexo generate && node deploy.js"
},
"dependencies": {
"aws-sdk": "^2.233.1",
"hexo": "^3.2.0",
"hexo-generator-archive": "^0.1.4",
"hexo-generator-category": "^0.1.3",
"hexo-generator-index": "^0.2.0",
"hexo-generator-tag": "^0.2.0",
"hexo-renderer-ejs": "^0.3.0",
"hexo-renderer-marked": "^0.3.0",
"hexo-renderer-stylus": "^0.3.1",
"hexo-server": "^0.2.0"
}
}

Do you want to comment on this post?

By default, comments are not enabled in Hexo. To enable them you need to head off to see those nice people over at Disqus and get yourself a free tier account. Then you can come back to your code base and add just one single line to the _config.yml file and rebuild the site and - comments!

After enabling comments for this site, I had to take a break and have a ciggie. Never has a comments system setup been so easy for me.

A big hat-tip and shoutout has to go to Ms/Mr Dreaming Engineer for their post explaining how to perform this special unicorn magic.

And now I’m done …