Fully Functional Jekyll Blog
There’s comes a time when tutorials on getting started aren’t enough. This is not one of those tutorials. After this article, you will have the ability to make a fully functional Jekyll blog, complete with pagination and search capabilities. Get ready to see just how awesome Jekyll can be.
Audience
This is for:
- People who have basic knowledge of Jekyll (Note: If you don’t read the docs. It’s easy to get to basic.)
- Know how to use a text editor
- Can do
gem install
- Are very good at HTML and JavaScript
- Understand Liquid templating (Again…see the docs if not)
Resources
If at any point you are lost or confused, these links should help you out:
Go!
To make sure we’re all on the same page, I am going to be using Jekyll version 2.5.3. So, if you haven’t yet, you should gem install jekyll -v 2.5.3
.
We will not be using the jekyll new
command to create this blog. It’s great for people who want to get the feel of a Jekyll blog, but I feel that it takes away from the learning experience. Instead, let’s start off by creating a fresh git repository and adding a config.yml file to it. This config.yml is where all of the site’s configuration goes. Variables in this file can be accessed with the global site
variable (e.g. site.foo
).
$ mkdir jekyll-blog
$ cd jekyll-blog
$ git init
$ git checkout -b gh-pages # for GitHub pages support
$ echo "2.2.2"; > .ruby-version # sets Ruby version
$ touch _config.yml
The YAML file:
# _config.yml
title: A Sample Blog # your blog's name
author: Jesse Herrick # your name
baseurl: /blog/ # we'll get to this later
exclude: # things to exclude from the build process
- README.md
- .ruby-version
- Gemfile
- Gemfile.lock
Now, some git to make sure we start off right:
$ git add .
$ git commit -m "Initial blog configuration"
$ git push -u origin gh-pages # you should create a GitHub repo if you haven't yet
Cool stuff. Instead of our master
branch being our deployment branch, gh-pages
will serve that role.
Before we just jump right in, let’s look at what we want in our blog:
- An index page of posts
- A page for individual posts
- The ability to search posts
We’ll need some actual posts to test with, so I generated a few using a Lorem Ipsum generator. For your convenience, you can get them here. Put these in a directory called, _posts.
Every page in Jekyll needs a default template, so make one before creating the post index page. This saves us from having to write all the obligatory stuff:
$ mkdir _layouts
$ touch _layouts/default.html
And the HTML:
# _layouts/default.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{% if page.title %}{{ page.title }} &· {% endif %}{{ site.title }}</title>
{% include libs.html %}
</head>
<body>
{{ content }}
</body>
</html>
You’ll notice in the title
tag that we used an if
statement:
{% if page.title %}{{ page.title }} &· {% endif %}
This states: if the page variable, title
is defined, then render it alongside a separator. If not, then nothing will be shown.
We also used the include
liquid tag to pull in an external HTML file called libs.html that lives in the _includes directory and contains our libraries. It’s possible to separate every template into smaller ones, but, at a certain point, it gets to be more time consuming than it’s worth.
# _includes/libs.html
<!-- Stylesheets -->
<link href='//fonts.googleapis.com/css?family=Arvo:400,400italic,700' rel='stylesheet' type='text/css'>
<link href="{{ '/css/style.css' | prepend: site.baseurl }}">
<!-- JS -->
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
<script src="{{ '/js/app.js' | prepend: site.baseurl }}"></script>
Awesome. Notice that we used the liquid filter, prepend
, to prepend our site’s previously defined base URL. It’s defined as /blog/
so that the assets load properly if the site is hosted at URL like http://johndoe.github.io/blog/
. This is helpful because our default layout is going to be applied to all pages.
Now let’s make the index page.
The index page needs to display whole posts and paginate every 3 posts (we have 4). Also, it should have links to individual post pages. Jekyll makes this super easy.
# index.html
---
title: Home
layout: default
---
<h1>{{ site.author }}'s Blog</h1>
<section class="posts">
{% for post in site.posts %}
<li><date>{{ post.date | date: "%B %-d, %Y"}}<a href="{{ post.url | prepend: site.baseurl }}">{{ post.title }}
{% endfor %}
</section>
It’s just a plain and simple index page with a classic for..in
loop over the posts.
Search
The AngularJS ng-repeat
directive is perfect for a quick live search of posts. But first, we need some sort of API for Angular to consume. Jekyll can actually do this for us in pure liquid. Because the site is statically generated, the data in our “API” doesn’t need to be dynamically generated either. So, to generate the API, just use the following code:
# posts.json
---
layout: null
---
{
"posts": [
{% for post in site.posts %}{
"title": "{{ post.title }}",
"url": "{{ post.url | prepend: site.baseurl }}",
"date": "{{ post.date | date: "%B %-d, %Y" }}",
"raw_date": "{{ post.date }}"
}{% unless forloop.last %},{% endunless %}
{% endfor %}
]
}
It’s pretty simple and generates a nice API for our blog that looks something like this:
{
"posts": [
{
"title": "Sample 4",
"url": "/2015/05/20/Sample-4.html",
"date": "May 20, 2015",
"raw_date": "2015-05-20 00:00:00 -0400"
},
{
"title": "Sample 3",
"url": "/2015/05/18/Sample-3.html",
"date": "May 18, 2015",
"raw_date": "2015-05-18 00:00:00 -0400"
},
{
"title": "Sample 1",
"url": "/2015/05/17/Sample-1.html",
"date": "May 17, 2015",
"raw_date": "2015-05-17 00:00:00 -0400"
},
{
"title": "Sample 2",
"url": "/2015/05/15/Sample-2.html",
"date": "May 15, 2015",
"raw_date": "2015-05-15 00:00:00 -0400"
}
]
}
So how do we use this in our blog to add search functionality?
# js/app.js
angular.module('JekyllBlog', [])
.controller('SearchCtrl', ['$scope', '$http', function($scope, $http) {
$http.get('/posts.json').success(function(data) {
$scope.posts = data.posts;
});
}]);
Now, add this to the index page, but there’s a problem: Jekyll will parse AngularJS’ brackets.
<p>{{wow.something}}</p>
<!-- is parsed into... -->
<p></p>
This is because Jekyll runs all templates through Liquid, which assumes (as it should) that all brackets are part of the Liquid templates. So, we have to get around this. One option is this plugin that I wrote, but since we plan on deploying to GitHub pages, custom plugins aren’t an option. We’ll have to use Liquid’s {% raw %}
tags (trust me, not as fun).
# index.html
<div class="posts" ng-controller="SearchCtrl">
<input type="search" class="search" ng-model="query">
<ul>
<li ng-repeat="post in posts | filter:query ">
<date ng-bind="post.date"></date>
<a href="{% raw %}{{ post.url }}{% endraw %}" ng-bind="post.title"></a>
</li>
</ul>
</div>
The special sauce here is the combination of ng-repeat
and a filter
. We’re basically telling Angular to display each item in the posts
array (aliased as post
) that matches the query
scope variable. You’ll notice that, at the top of the list we added a search input bound by ng-model
to the query
scope variable. Then we just reuse same output as used earlier in {% for post in posts %}
, but using the Jekyll “API”.
What About People Who Don’t Have JS?
If you really need to cater to those people that don’t have JavaScript enabled, you can use a little ng-cloak
magic.
# css/styles.css (as recommended by: https://docs.angularjs.org/api/ng/directive/ngCloak)
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
display: none !important;
}
# index.html
<div ng-cloak class="posts" ng-controller="SearchCtrl">
<input type="search" class="search" ng-model="query">
<ul>
<li ng-repeat="post in posts | filter:query ">
<date ng-bind="post.date"></date>
<a href="{% raw %}{{ post.url }}{% endraw %}" ng-bind="post.title"></a>
</li>
</ul>
</div>
<div ng-show class="posts">
<ul>
{% for post in posts %}
<li><date>{{ post.date | date: "%B %-d, %Y"}}<a href="{{ post.url | prepend: site.baseurl }}">{{ post.title }}</a></li>
{% endfor %}
</ul>
</div>
This works through some interesting logic. First, AngularJS’ ng-cloak
directive is detected by some CSS styles and is hidden until AngularJS is loaded. Thus, if JS is disabled, nothing is shown.
Next, the ng-show
directive is added to the Jekyll-generated list of posts. This works because AngularJS defaults ng-show
to false
, meaning that the Jekyll list will only be shown if AngularJS is not loaded.
What About Posts?
Each post has a link, but we haven’t really talked about what happens when you click that link. Right now, it displays the post’s markdown rendered to HTML, but we want more. First, let’s add a default layout to all of our posts because we shouldn’t have to worry about that when writing a new post:
# _config.yml
... other config ...
defaults:
-
scope:
path: ""
type: "posts"
values:
layout: "post"
This means that layout: post
will be added to all post
type files. Now we need to create this post layout:
$ touch _layouts/post.html
This layout only really needs a heading and a place to render the post:
# _layouts/post.html -->
<article>
<h2>{{ page.title }}</h2>
<small>Written by <strong>{{ site.author }}</strong><date>{{ page.date | date: "%B %-d, %Y" }}</date>.</small>
{{ content }}
</article>
If you’re familiar with Rails, {{ content }}
is like in terms of layouts. In this case, it is yielding to content passed in each post. Notice that we also used
page.title
and page.date
rather than post.title
, etc. because we are calling a variable that is only passed to the page (layout) and not through an iteration.
Now, if you visit a post link, a nice heading and some meta info about the post on that page are displayed.
Conclusion
There you have it! A fully functional Jekyll blog. A little more styling and you have something to write use to your heart’s desire. If you’d like to learn more about what Jekyll can do, check out the docs. Happy Blogging!