Building this website - iameven.com
How I build... err, how I used to build my website.
This document is obsolete, but I use some of the same techniques described here in my new system
I changed back to Jekyll for this blog, because I found a cool theme. Dokku also used to have a Jekyll buildpack, but it looks like they removed it. They've also removed the handy Nginx pack for static file serving. This is something I didn't discover before starting the transition. Either way I had to find another solution.
A web server in 15 lines
Using Node.js and express I've found a handy trick to create a static file server:
var express = require('express'),
app = express(),
port = process.env.PORT || 4000;
// this is where the magic happens
app.use(express.static(__dirname + '/public'));
// catch unresolved paths and serve a 404.html file
app.use(function(req, res) {
res.status(404).sendFile(__dirname + '/public/404.html');
});
app.listen(port, function() {
console.log('listening on port ' + port);
});
I have configured Jekyll to build to the '/public' directory, and use express to serve any files in that folder. It even serves 404.html from that folder if it can't resolve the path.
Optimizing the theme
I think I'm a bit obsessed with optimization, but I find bought themes to never do their absolute best. Font Awesome was included, in full. Yet only 15 of the 479 icons are actually used by the theme. Fontello is a service that creates a custom font build by combining icons freely from several packs, including custom SVG files. Making my custom font reduces the Woff file chrome downloads from 65KB to 5KB, that is 13 times smaller.
Four web fonts gets included from Google, I'm not sure I want to do anything about that because I like the fonts in this theme. It comes as no surprise, jQuery.js is also included, as a separate file which it usually is. Best practices these days dictate combining JavaScript files to reduce HTTP calls.
Libraries
As cool as the theme is, it didn't include any light box. I found strip.js recently and like the original take on the light box. I also have some audio posts, and mediaelement.js is a pretty cool player, with flash and silverlight fall backs (that I haven't included, yet). These libraries come with custom CSS and images. I decided to combine it all in one CSS and one JS file.
I use Bower to download jQuery.js, mediaelement.js and strip.js.
Concatenating and minimizing libraries
I've experimented with only including JavaScript in posts that need it. But that requires some maintenance which I don't want to do.
Using Grunt.js
With many packages Grunt.js repeats tedious tasks once you've told it how to, with a Gruntfile. I'm splitting the Gruntfile up here to explain it (and the nesting breaks up the code in my preview), but you can have a look at my complete Gruntfile (2014-11-02).
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
// this is where the tasks go
});
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-replace');
grunt.registerTask('default', ['copy', 'replace', 'concat', 'uglify']);
};
loadNpmTasks load npm libraries, the include can happen anywhere, so I put it in the end of the file to get them out of the way. There are tricks to read the dependencies from package.json and load them all, but I think this gives me better control of which tools I'm using.
registerTask defines which jobs grunt should do. The name 'default' is the one grunt should run without any parameters.
# run default tasks
grunt
# or
grunt default
# run a single task
grunt copy
The array just defines which tasks to run, in my instance it's all the jobs. There might be a better way to select everything, but I haven't looked into it.
Copy
grunt-contrib-copy copies the individual CSS files, and renames them to SCSS in my Jekyll folder. I also copy all the image dependencies.
// grunt.initConfig ({ ...
copy: { main: {
files: [
{
expand: true,
cwd: 'bower_components/mediaelement/build/',
src: 'mediaelementplayer.CSS',
dest: 'jekyll/assets/CSS/',
filter: 'isFile',
rename: function(dest, src) {
return dest + src.replace('.CSS', '.SCSS');
}
},
{
expand: true,
cwd: 'bower_components/strip/CSS/',
src: 'strip.CSS',
dest: 'jekyll/assets/CSS/',
filter: 'isFile',
rename: function(dest, src) {
return dest + src.replace('.CSS', '.SCSS');
}
},
{
expand: true,
cwd: 'bower_components/mediaelement/build/',
src: ['*.png', '*.svg'],
dest: 'jekyll/assets/img/',
flatten: true
},
{
expand: true,
cwd: 'bower_components/strip/CSS/strip-skins/strip/',
src: ['*.png', '*.svg'],
dest: 'jekyll/assets/img/',
flatten: true
}
]
I don't completely understand the "expand" boolean, but it is required to use "cwd". "cwd" excludes parts from being copied, in this case the folder hierarchy. Without it I end up with "jekyll/assets/CSS/bower_components/mediaelement/build/" in the first task. Now it is put in the "jekyll/assets/CSS" folder. "src" is where I copy from and "dest" is destination, "filter" tells copy that this is a single file, but I don't think it does anything. My hope was that "filter" would avoid copying folders, but I had to use "cwd" for that. Rename changes the extensions from CSS to SCSS. When SCSS include other SCSS files they are combined into one file, but CSS files get included like external files, which I don't want, saving two http requests. The two last tasks copies all image dependencies. "flatten" is there to prevent folder hierarchy, but I had to use "cwd" here as well.
Replace
Since I change image paths which the CSS files reference I have to do some regex with grunt-replace.
// grunt.initConfig ({ ...
replace: { dist: {
options: {
patterns: [
{
match: /'strip\-skins\/strip\//g,
replacement: ""
},
{
match: /'\)/g,
replacement: ")"
},
{
match: /url\(/g,
replacement: "url(../img/"
}
]
},
files: [
{expand: true, flatten: true,
src: [
'jekyll/assets/CSS/mediaelementplayer.SCSS',
'jekyll/assets/CSS/strip.SCSS'
],
dest: 'jekyll/assets/CSS/'}
]
The "match" are specific to the libraries I include, I use the same definition on both files to save myself some headache. I just had to think about the order. I move all the images to a sibling folder of the CSS folder named "img". In strip all image includes look like this:
url('strip-skins/strip/image.png');
The first two patterns removes "'strip-skins/strip/" and the ending "'", because they are not needed and it made my job easier, since now both the files are similar and assumes all images are in the same path as the CSS file. The third pattern inserts "../img/" to point up one folder and into the "img" folder. The "url(" is the unique string I search for to do this, which I insert again with the path.
javascripts
I tried to do this using only grunt-contrib-uglify, but it didn't order the files correctly and jQuery functions was not found by JavaScript depending on it, making it break in the browser. So I used grunt-contrib-concat as well. Yet again to save myself some headache.
// grunt.initConfig ({ ...
concat: {
options: {
separator: ';'
},
dist: {
src: [
'bower_components/jquery/dist/jquery.js',
'bower_components/mediaelement/build/mediaelement-and-player.js',
'bower_components/strip/js/strip.pkgd.js',
'jekyll/assets/js/theme.js'
],
dest: 'jekyll/assets/js/iameven.js'
}
The options separator makes sure there is a ";" between the files to not break any of the scripts. This concatenate library is handy because it combines the files in the order given. I thought that was a sane default, but Uglify doesn't do that.
// grunt.initConfig ({ ...
uglify: { my_target: {
files: {
'jekyll/assets/js/iameven.min.js': ['jekyll/assets/js/iameven.js']
}
The first part in files is the output and the second an array of all the files it should combine and uglify. "iameven.min.js" is the file that is included on this site, so if I ever do changes to my theme.js I just rebuild it all. If I need more libraries I will have to add to my Gruntfile. I also hope that the structure won't change to much in the included libraries if I ever update them.
Posting
Making and publishing a post requires 7 steps:
poole draft "Post"
sublime _drafts/post.md # Obviously a big step involving some sub steps and time.
poole publish _drafts/post.md
jekyll build
git add --all
git commit -am "published Post"
git push dokku master
Mr. Poole is Jekylls butler and is a handy Gem to work with posts. Create draft or post, publish if draft and unpublish if post (and I want to redact it). It is handy because Jekyll requires dates in the file names, and Mr. Poole renames as it moves these around.
Sublime Text 2 is my text editor of choice.
Recap
I use Grunt to build a single minimized JavaScript file. Then collect images from libraries, and alter CSS files while moving them to a folder. I then use Jekyll to build my HTML files and CSS file. I add the files to Git and Push to my remote web server. In my Git repository there are two files in its root to instruct Dokku:
- package.json
- Procfile
The package file tells Dokku that this is a Node.js project. It then downloads Node.js and all the dependencies listed in the package file. The Procfile tells Dokku which tasks to run, in my instance:
web: node index.js
The index file is the static file server I mentioned near the top of this post.
Ruby and Node.js
I do like this setup, it works, but there are plenty of static blog generators for Node.js. Being dependant on two programming languages and ecosystems seems redundant. However, having just changed it's not something I want to immediately repeat unless i find something resembling Jekyll in structure.
Nag.js
I want to keep updating this site, but the nature of it is very much set and forget. So as an experiment I've created a script to send myself some reminders using mailgun. It checks when the site was last updated, every hour, which probably is more than I need. When a set time is reached it send me an email to tell me how many days it's been since my latest build.
- week
- fortnight
- 18 days
- 22 days
- 26 days
- every day > 26 days
It only checks my latest build time, and not my latest post, so as long as I do changes I wont get nagged. But when I do changes I do remember the site, which is sort of the point. So we'll see how well this works. The ultimate goal is to get more frequent new posts.