Quantcast
Channel: Terminally Incoherent » html5
Viewing all articles
Browse latest Browse all 3

Building a Jekyll Site

$
0
0

Back in 2009 I got a brilliant idea into my head: I was going to build a site on top of Joomla. Why? I still don’t exactly understand my own thought process that lead me to that decision. I think it had something to do that it was branded as a content management system and I had some content I wanted to manage. Perhaps it was because it looked serious and enterprisey and I wanted to try something different hoping it would be less of a pain in the ass than WordPress. Or perhaps it was a bout of temporary insanity.

Don’t get me wrong, Joomla is a wonderful CMS with a billion features that will let you do just about anything, but typically in the least convenient and most convoluted manner. I’d be tempted to say that Joomla engineers never actually tested their software on live humans before pushing it out into the public but I suspect that’s wrong. I suspect they have done a ton of usability testing, and purposefully picked the least friendly and the most annoying user experience. Because, fuck you for using Joomla.

Granted, they might have made some great improvements since 2009, but I wouldn’t know because upon slapping it on the server, and vomiting my content all over it, I decided I never actually want to touch anything on the Admin side of it ever again. On day two of my adventure with Joomla I decided that shit needed to go, but since I just manually migrated (read copy and pasted) like dozens of Kilobytes of content into it, I couldn’t be bothered. So I took out my scheduling book, penciled the site upgrade for “when I get around to it” and then threw the book out the window, because scheduling things makes me sad and hungry which is why I never do it.

Fast forward to 2014 and I was still happily “getting around to it”, when my host sent me a nasty-gram saying my Joomla is literally stinking up their data center. I had no clue what they were on about, since the installation was pristine clean, vintage 2009 build in a virgin state of never having been patched, updated or maintained. But since they threatened to shut all of my things down unless I get that shit off their server I decided it was time. I got around to it.

Fist step was straight up deleting Joomla. Second step was picking the right toolkit for the job. I briefly considered WordPress, but that’s a whole other can of worms, but for different reasons. WordPress is actually pretty great as long as no one is reading your blog. As soon as you get readers, the fame goes to it’s head and it decides it owns all the memory and all of the CPU time on the server, and demands a monthly sacrifice of additional Rams as your user base grows. It is literally the bane of shared servers, and most WordPress “optimization” guides start by telling you to abandon all that you know, and run like seventeen layers of load-balanced proxy servers in front of it. Not that Joomla performance is any better, but that site had no readers so it was usable. But since I was getting around to updating it, one of the goals was making it more robust and scalable, rather than trading a nightmarish clustefuck of crap for a moderately unpleasant pile of excrement. I figured I might as well go for broke and trade it for something good: like a mound of fragrant poop or something.

Since the site was on a shared host with a Quintillion users, and I didn’t feel like paying for and setting up yet another droplet I opted for a statically generated site. I tried a few static site generators and Jekyll is the one that did not make me want to punch the wall in the face (if it had a face) so I opted for that. Plus, I already had some basic layout done, so I figured I might as well use it.

The huge benefit of having a static site running on a shared host is that in theory you will never have to touch it, other than to update the content. The host will take care of the updating the underlying OS and web server, and since you have no actual “code” running on your end, there is noting there to break. Once you put it up, it can run forever without framework or plarform upgrades. It is a low effort way to run a site.

As far as front end went, I knew I wanted to work with HTML5 and that I wanted a grid based systems because making floating sidebars is a pain in the ass. So I whipped out Bower and installed Bootstrap 3.

I know what you were going to say: fuck Bootstrap, and I agree. Bootstrap is terrible, awful and overused. In fact, I think the authors of the project realized how much of a crutch it is, which is why they introduced a conflict with Google Custom Search in the latest version. Bootstrap literally breakes Google’s dynamic search box code, because fuck you for using Bootsrap.

But, it’s easy, clean and I love it, so I bowered it. Bootstrap consumes jQuery as a dependency so I got that for free. This is another useful framework people love to shit all over (though for good reasons) but since I already got it I figured I might as well use it for… Something.

One crappy thing about Bower is that when it fetches dependencies it puts all of them in bower_components directory, including useless garbage such as Readme files, build files and etc. Some people package their distributable code for Bower, but most projects don’t give a shit and just use their main repository and give you un-compressed, un-minified files along with all the associated miscellaneous garbage. I loathe having Readme files showing up on a deployment server, so I decided to manually minify and concatenate my scripts and stylesheet with Bootrstrap ones. For
that I needed Grunt. For grunt I needed Node. And so it goes. It is funny how one decision cascades into a dependency tree.

Runtime Dependencies

Pretty much at the onset, I decided I will be using the following:

Only the first item on the list is something you would want to install locally. The rest can be run off a CDN pretty reliably. Actually, you could run jQuery off a CDN too, but I decided not to. This makes your bower.json incredibly simple:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "name": "My Site",
  "version": "0.0.0",
  "authors": [
    "Luke Maciak <my@email.com>"
  ],
  "description": "Blah blah blah, website",
  "license": "MIT",
  "homepage": "http://example.com",
  "private": true,
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "test",
    "tests"
  ],
  "dependencies": {
    "bootstrap": "~3.1.1"
  }
}

This is the nice thing about static sites. Your production does not need a lot of setup – you just copy the files over and your done. All the heavy lifting is done at development time.

Dev Dependencies

Here our list is longer. I need things to build the code, manage the dependencies and some way of deploying it all to a server in a non annoying way.

  1. Ruby and Gems
  2. Jekyll
  3. Node and NPM
  4. Grunt
  5. rsync for moving the files around between servers

I already had Ruby, Jekyll node and Grunt running on my system because… Well, why wouldn’t you. I mean, that’s sort of basic stuff you install on the first day when you get a new computer. So all I had to do was to steal a project.json file from another project and slap it in my directory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "author": "Luke Maciak",
  "name": "My Website",
  "version": "1.0.0",
  "dependencies": {},
  "devDependencies": {
    "grunt-html-validation": "~0.1.6",
    "grunt-contrib-watch": "~0.5.3",
    "grunt-contrib-jshint": "~0.1.1",
    "grunt-contrib-uglify": "~0.1.1",
    "grunt-contrib-concat": "~0.1.3",
    "grunt-contrib-cssmin": "~0.5.0",
    "grunt-contrib-csslint": "~0.1.2",
    "grunt-contrib-copy": "~0.4.1",
    "grunt-shell": "^0.7.0"
  }
}

Once it was in place, fetching all the grunt dependencies was a matter of running npm install. Now comes the hard part: setting up your Gruntfile.

Basic Setup

For the sake of completion, here is my complete Gruntfile

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
/*global module:false*/
module.exports = function(grunt) {
 
    // Project configuration.
    grunt.initConfig({
        validation: {
            options: {
                reset: grunt.option('reset') || true,
            },
            files: "_site/**/!(google*).html"
        },
    watch: {
        files: "<config:htmllint.files>",
        tasks: 'validate'
    },
    jshint: {
      files: [  'Ggruntfile.js', 
                'scripts.js'
             ],
      options: {
        white: false,
        curly: true,
        eqeqeq: true,
        immed: true,
        latedef: true,
        newcap: true,
        noarg: true,
        sub: true,
        undef: true,
        boss: true,
        eqnull: true,
        smarttabs: true,
        browser: true,
        globals: {
            $: false,
            jQuery: false,
 
            // Underscore.js
            _: false,
 
            // Chrome console
            console: false,
 
          }
      },
    },
    csslint: {
        lint: {
            options: {
               'ids': false,
               'box-sizing': false
            },
            src: ['style.css']
        }
    },
    cssmin: {
        compress: {
            files: {
                'style.tmp.min.css': ['style.css'],
            }
        }
    },
    concat: {
        options: {
            separator: ';' + grunt.util.linefeed,
            stripBanners: true,
        },
        js: {
            src: [
                    'bower_components/jquery/dist/jquery.min.js',
                    'bower_components/bootstrap/dist/js/bootstrap.min.js',
                    'scripts.tmp.min.js'
            ],
            dest: 'resources/js/scripts.min.js'
        },
        css: {
            src: [
                    'bower_components/bootstrap/dist/css/bootstrap.min.css',
                    'style.tmp.min.css'
            ],
            dest: 'resources/css/style.min.css'
        }
    },
    copy: {
        main: {
            files: [  
                {   expand: true, 
                    flatten: true,
                    src: 'bower_components/bootstrap/dist/fonts/*', 
                    dest: 'resources/fonts', 
                    filter: 'isFile'
                }
            ]
        },
    },
    uglify : {
        main: {
                 src: ['scripts.js'],
                 dest: 'scripts.tmp.min.js'
             }
    },
    shell: {
        jekyll: {
            command: 'jekyll build'
        }
    }
    });
 
    grunt.loadNpmTasks('grunt-contrib-watch');
    grunt.loadNpmTasks('grunt-html-validation');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-jshint');
    grunt.loadNpmTasks('grunt-contrib-concat');
    grunt.loadNpmTasks('grunt-contrib-cssmin');
    grunt.loadNpmTasks('grunt-contrib-csslint');
    grunt.loadNpmTasks('grunt-contrib-copy');
    grunt.loadNpmTasks('grunt-shell');
 
    grunt.registerTask('default', ['jshint', 'uglify', 'csslint', 'cssmin', 'copy', 'concat']);
    grunt.registerTask('all', ['default', 'shell', 'validation']);
};

It is a huge, monolithic pile of configuration so let me explain what I’m trying to accomplish here. In an ideal world, you want to have a single CSS file linked at the top of your page, and a single JavaScript file linked on the bottom. If you use Bower to handle dependencies (as you should) this is not possible, because every little thing you install gets it’s own folder in bower_components folder. So your first task is to pick out the important parts from each of those folders, smush them together into these two files. This is what is happening here.

For example, lines 56-62 run my custom CSS rules (style.css) through a minifier (using grunt-contrib-ccsmin) that removes all the spaces, and makes it super-ugly for the purpose of loading faster. Likes 96-101 do the exact same thing to my custom JavaScript code in scripts.js via grunt-contrib-uglify. So I end up with two very ugly files style.tmp.min.css and scripts.tmp.min.js. All of these files will be excluded from Jekyll compilation via _config.yml exclude list.

Once I have those, I use the grunt-contrib-concat plugin to concatenate my custom stylesheets and scripts with those provided by Bootstrap. You can see that in lines 63-83. I end up with my two ideal production ready files named: style.min.css and scripts.min.css. The new files are placed in resources/ directory.

A side effect of re-locating the Bootstrap script and CSS is that you break the glyphicons. The css files have relative paths to the web-font included in the bootstrap package, so if you want it to work it has to be in the fonts/ directory relative to the css location. This is what the 84-95 section is about. I’m taking all the files from bootstrap_components/dist/fonts/ and placing them in resources/fonts/ like this:

resources/
├── css
│   └── style.min.css
├── fonts
│   ├── glyphicons-halflings-regular.eot
│   ├── glyphicons-halflings-regular.svg
│   ├── glyphicons-halflings-regular.ttf
│   └── glyphicons-halflings-regular.woff
└── js
    └── scripts.min.js

The rest of the file is mostly concerned with linting. I check my css code with grunt-contrib-csslint and my JavaScript with grunt-contrib-jshint which is fairly standard. In both cases I’m relaxing the linting notes a little bit to preserve my own sanity, and to get around ugly hacks. For example ‘box-sizing': false on line 51 is there to allow me to fix the aforementioned css that completely breaks Google’s Custom Search functionality. Similarly on line 35 I’m declaring $ as a global, because JSHint does not uderstand jQuery and freaks out for no reason.

I’m also using the excellent grunt-html-validation plugin to make sure my HTML is valid.

Finally, here is my _config.yaml file for Jekyll. It is mostly unremarkable, save for the exclusion list where I prevent Jekyll from copying all of the useless files into production.

name: My Site
description: blah blah blah
author: Luke

category_dir: /
url: http://example.com

markdown: rdiscount
permalink: pretty
paginate: 5

exclude: [
            package.json, 
            bower.json, 
            grunt.js,
            Gruntfile.js, 
            node_modules, 
            bower_components,
            validation-report.json, 
            validation-status.json,
            scripts.js, 
            scripts.tmp.min.js,
            style.css,
            style.tmp.min.css,
            lgoo.psd,
            Makefile,
            exclude.rsync,
            README.markdown
         ]

Grunt takes care of compiling and linting all the front end code, while Jekyll builds the site from an assortment of html and markdown files. I already wrote a lengthy article about setting up a basic Jekyll site before, so I won’t bore you with the details here. The basic skeleton looks like this though:

.
├── _config.yml
├── _drafts/
├── _layouts/
│   ├── category_index.html
│   ├── default.html
│   ├── page.html
│   └── post.html
├── _plugins/
│   ├── generate_categories.rb
│   └── generate_sitemap.rb
├── bower.json
├── bower_components/
│   ├── bootstrap/
│   └── jquery/
├── exclude.rsync
├── favicon.ico
├── feed.xml
├── Gruntfile.js
├── imag/
├── index.html
├── Makefile
├── node_modules/
├── package.json
├── README.markdown
├── resources/
│   ├── css/
│   ├── fonts/
│   └── js/
├── robots.txt
├── _site/
├── scripts.js
└── style.css

The bower_components directory as well as the “naked” JavaScript and CSS files are excluded from compilation, in lieu of the resources which contains files generated by Grunt. Other than that this is a fairly standard structure.

Deployment

As I said before I decided to use rsync to deploy the website. There are many ways to deploy a Jekyll website, but this is probably the most efficient tool for the job. In an ideal world, you compile a Jekyll site, and then rsync compares your _site directory to what is on the server and only copies/deletes files that are different. This means you first upload will be massive, but from that point on, you are just going to transfer the deltas.

There is a little caveat here though: by default rsync compares files based on timestamps. This is a problem because Jekyll clobbers the _site directory every time you build your site. This means that every file inside of it will look brand spanking new to rsync even if it has not technically changed. This downgrades our delta-sync tool to just a crude file uploader that is no more sophisticated than an rm -rf command followed by scp _site/* host:~/site.

Fortunately, I found an excellent tip by Nathan Grigg which suggests telling rsync to use checksums instead of timestamps. By force of habit, when setting up an rsync script most of us might be tempted to write something like:

rsync -az --delete _site/* user@host:~/site

This is the traditional and wrong way of doing this. What Nathan suggests instead is:

rsync -crz --delete _site/* user@host:~/site

Or perhaps, more descriptively:

rsync --compress --recursive --checksum --delete _site/* luke@myhost:~/site/

I actually like to use the long arguments when I write scripts, because years down the road they will make it easy to understand what is going on without looking up cryptic letter assignments in the man pages.

To simplify deployment I wrote myself a little Makefile like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.PHONY: check, deploy, tunnel-deploy, build
 
default: check, build
 
check:
	@command -v ssh >/dev/null 2>&1 || { echo "ERROR: please install ssh"; exit 1; }
	@command -v rsync >/dev/null 2>&1 || { echo "ERROR: please install rsync"; exit 1; }
	@command -v grunt >/dev/null 2>&1 || { echo "ERROR: please install grunt"; exit 1; }
	@command -v jekyll >/dev/null 2>&1 || { echo "ERROR: please install jekyll"; exit 1; }
	@[ -d "_site" ] || { echo "ERROR: Missing the _site folder."; exit 1; }
 
build: check
	grunt
	jekyll build
 
deploy: check, build
	rsync --compress --recursive --checksum --delete --itemize-changes --exclude-from exclude.rsync _site/* luke@myhost:~/site/
	ssh luke@myhost 'chmod -R 755 ~/site'

Tagging

Jekyll kinda supports tags and categories, but those are still rather underdeveloped features. When I build Jekyll sites I like to use Dave Perret’s plugin to get nice category archive pages. It also injects the category names into the “pretty” permalinks adding taxonomy to your url structure.

I have a very specific idea abut how tags and categories should be handled and how they differ. For me, categories group posts of a certain broad type, while tags are used to indicate specific topics/keywords that cut across the categories. So for example, you could have a category named “videos” and bunch of tags like “interview”, “trailer”, etc.. That said, the tag “interview” is not unique to the “videos” category and could also be used to tag posts in other categories like “pictures” for example. I like to have one category per post, but multiple tags. These is not a hard rules and most systems out there allow for more liberal use of both concepts. Dave’s plugin actually allows for multiple categories per post. But I typically stick to one. It is a personal preference of mine.

Categories are big, broad and there are few of them. I will often list them on the sidebar, and use them for navigation. Tags are different – they are more messy. So I opted to have a single page that would essentially be a table of contents by tag.

Michael Lanyon did an excellent writeup on how to alphabetize your tag list using nothing but Liquid tags in your template.

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
{% capture site_tags %}{% for tag in site.tags %}{{ tag | first }}{% unless forloop.last %},{% endunless %}{% endfor %}{% endcapture %}
{% assign tag_words = site_tags | split:',' | sort %}
 
<div id="tags">
   <h3>Table of Contents:</h3>
      <ul>
         {% for item in (0..site.tags.size) %}{% unless forloop.last %}
            {% capture this_word %}{{ tag_words[item] | strip_newlines }}{% endcapture %}
               <li>
                  <a class="tag-link" href="#{{ this_word | cgi_escape }}">{{ this_word }} 
                  <span class="badge pull-right">{{ site.tags[this_word].size }}</span></a>
               </li>
            {% endunless %}{% endfor %}
      </ul>
 
   <h3>Posts for each tag:</h3>
 
   <div class="taglisting">
 
      {% for item in (0..site.tags.size) %}{% unless forloop.last %}
         {% capture this_word %}{{ tag_words[item] | strip_newlines }}{% endcapture %}
 
            <div class="tag-list" id="{{ this_word | cgi_escape }}" >
               <h4><i class="glyphicon glyphicon-tag"></i>
                  {{ this_word }}</h4>
 
               <ul class="posts">
                  {% for post in site.tags[this_word] %}{% if post.title != null %}
                     <li>
                        <i class="fa fa-calendar"></i>
                           <time datetime="{{ post.date | date:"%F" }}">
                              {{ post.date | date: "%b %d, %Y" }}</time>
                        <a href="{{site.baseurl}}{{ post.url }}">{{ post.title }}</a></li>
                  {% endif %}{% endfor %}
               </ul>
            </div>
      {% endunless %}{% endfor %}
   </div>
</div>

This works very nicely generating an alphabetized list that is easy to search through. You can link to the list for individual tag using a hashmark in the URL: http://example.com/tags/#tagname and it will take you to that section. That said, it can be a bit confusing for the user to get dumped into the middle of a huge list of unrelated things. So I built upon Dave’s idea and added some Javascript to the mix.

I figured I already have a jQuery dependence, so I might as well use it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var MySite = MySite || {};
 
MySite.showSingleTag = function showSingleTag() {
    $(".tag-list").hide();
    $(window.location.hash).show();
};
 
 
$( document ).ready(function() {
    if( window.location.hash )
    {
        MySite.showSingleTag();
    }
 
    $(window).on('hashchange', function() {
        Gigi.showSingleTag();
 
        // scroll to the element
        $('html,body').animate({scrollTop: 
            $(window.location.hash).offset().top},0);
    });       
});

This script detects if there is a hash in the URL, and if so hides all the entries, except the ones related to the relevant tag. I left the list of tags alone, because I figured the user might want to explore what else is available. Because of this a little bit of additional logic was added. If you click on a hash-link the browser page won’t reload, and thus my hashmark check won’t trigger. So on line 15 I check if the URL hash changes, and if so I re-do the hiding, and then I forcefully scroll the user’s viewport back to the tag list.

TL;DR

I have successfully switched from Joomla to Jekyll and it’s great. I’m totally not going to regret this choice 5 years down the road, right? I mean, what could go wrong, other than everything. Actually, I’m already begging to see cracks forming in this master plan. You see, the site has a lot of images. They are mostly low to medium resolution screen-shots, but there are a lot of them, and there will be many more if I actually keep updating this thing more than once a year. As part of the update I added about 100MB worth of images, which is not a terrible lot but it has slowed the Jekyll compilation times quite a bit. So this is bound to get super annoying real quick… But I guess that’s par for the course: all software sucks, and it is a fucking miracle the internet even works seeing how nearly every website in existence is held in place with a digital equivalent of duct tape.

You can see the fruits of my labor at gigiedgleyfansite.com. While it’s not perfect, I think it is a huge improvement over the old Joomla based site. Let me know what you think.


Viewing all articles
Browse latest Browse all 3

Latest Images

Trending Articles





Latest Images