Because I put a lot of time into this site in the last few months, I figured I might as well share my experience so others can jump right in as well! You can view the source for this site on the GitHub repository.
Note: This site is now built with Eleventy, but you can view it at the commit linked above to see it in its final Jekyll form.
Introduction
Jekyll is a static site generator, written in Ruby. It takes a series of templates and static files, and outputs to a ready to host directory of files. For simple websites like a blog or portfolio, a static site generator is perfect. Additionally, with a generator like Jekyll and its Liquid templating engine, you are still able to use many powerful features like markdown, layouts, partials, and iterating through data to build files. As an bonus, GitHub Pages can build a Jekyll site and host it, making it perfect for personal and project websites and blogs.
Installation
Jekyll officially supports Linux, Unix, and macOS, though it seems you can get it to install on Windows as well.
As Jekyll is written in Ruby, you will need Ruby installed. Apple ships the now unmaintained 2.0.0p648
version of Ruby on El Capitan, so I installed the latest version of Ruby (2.3.1
) with Homebrew (β€οΈ Homebrew, I love a good package manager). I deploy this site with GitHub Pages, so to start off, I installed the github-pages
gem. To maintain dependencies, create a file called Gemfile
and add the following:
source 'https://rubygems.org'
gem 'github-pages'
There is a nice tool called bundler
that manages packages and environments for you. This is the recommended way to use Jekyll (as far as I can tell). To install that and your project dependencies, run
gem install bundler
bundle install
Now you're all set and ready to start building your site! Check out the Jekyll installation docs for more info.
Getting Started
You can either create all the files yourself, or run the following to generate a new scaffold site:
jekyll new path/to/directory
Configuring and Structure
To build your site, run:
bundle exec jekyll build
# build the site
bundle exec jekyll serve --livereload
# build the site and serve it, auto-regenerating on changes
# access from http://localhost:4000/
By default, the site is built to _site
and is sourced from the current directory. These paths can be configured in _config.yml
or by passing in flags when running jekyll
.
_config.yml
To start off, you will need a _config.yml
file for the configuration, and a place to store global variables. Here is a good starter example for the file.
markdown: kramdown
timezone: America/Chicago
permalink: /posts/:slug
exclude:
- README.md
- .gitignore
- Gemfile
- Gemfile.lock
- .sass-cache
- CNAME
- LICENSE
The markdown: kramdown
line tells Jekyll to use kramdown as the markdown interpreter.
The timezone
setting will make sure dates used across the site will be localized to a timezone that you want, if not specified, Jekyll defaults to UTC.
The permalink
setting tells Jekyll how to route the blog posts. For historical reasons, I have mine at /posts/:slug
. You can format your permalink to include any information available on the page, ie. front matter or built in variables. Check out Jekyll permalink docs for much more information.
The exclude
setting tells Jekyll to ignore certain files when building the site and will not copy them to the _site
directory.
Front Matter
Front matter is a YAML block in-between a set of triple-dashed lines. It is how to set page level variables and metadata. Liquid will only add tags and variables if a file contains front matter. Below is the front matter from this post:
---
layout:
title:
slug:
modified:
description:
author:
seo.type: BlogPosting
image:
---
Variables included in the front matter or that come standard on any page or post can be accessed on the page by using the Liquid template engine:
<h1>{{ page.title }}</h1>
That's pretty neat, what else can you do?
Good question, I'm glad you asked!
{% assign author = site.data.authors[page.author] %}
<p>
Written by {{ author.name }} on {{ page.date |
date: "%B %-d, %Y" }}
</p>
Woah.
What's going on here? To begin, we use a data file to store authors. This can be found at _data/authors.yml
. Content in this file becomes available to Liquid at site.data.authors
. More info on data files can be found on the Jekyll data files docs. We're assigning the author
variable to site.data.authors[page.author]
. page.author
refers to the front matter above, which resolves to brian
. So now author
contains everything inside of the brian
object in _data/authors.yml
. Inside that file we have:
brian:
name: Brian Mitchell
bio: 'Web developer, emoji lover :wave:, and an avid fan of electronic music.'
twitter: BrianMitchL
gravatar: 89e0d7d3d9370c45517960c8a12f92b9
web: https://brianm.me
Information can be accessed just as if it were front matter, which is what {{ author.name }}
is doing above.
As you can see in the example above, Liquid can also filter data. Here, the date is being formatted. This is just the beginning of what Liquid can do, but it's two things that I think are really useful. Checkout other filters and Liquid features on the Jekyll template docs.
If you have a file that you don't need to declare any variables, but want to use Liquid you must still use the set of triple-dashed lines for Jekyll to process that file. For example, here's my humans.txt file:
---
---
/* TEAM */
Name: {{ site.data.authors.brian.name }}
Twitter: @{{ site.data.authors.brian.twitter }}
Location: Minneapolis, MN, USA.
/* THANKS */
Name: mom
/* SITE */
Last update: {{ site.time | date: "%Y/%m/%d"}}
Language: English
Doctype: HTML5
Components: Jekyll, UIkit, fontawesome, SCSS, GitHub
Software: WebStorm, Safari, Photoshop
{%- endraw -%}
For a full list of features, check out the Jekyll docs on front matter and variables.
Layouts and Includes
Creating reusable templates through layouts and includes are crucial to building your site in a reusable and maintainable way.
Layouts
When writing pages and posts, you don't want to have to keep track of every occurrence of a navbar, or page head. This is where a layouts and includes are extremely useful. Store any layouts you use in _layouts/
. In my site, I have a file at _layouts/default.html
which more or less contains the following:
{%- raw -%}
<!doctype html>
<html lang="en">
<head>
{% include head.html %}
</head>
<body>
{{ content }} {% include footer.html %}
<!-- some script tags and Google Analytics are located here -->
</body>
</html>
{%- endraw -%}
Every page I make in some way uses this layout. If I created a new page, I would include the following in its front matter, and the content of that file would be rendered into the {% raw %}{{ content }}
of the default layout:
---
layout: default
---
Another layout can also use a layout. It's content will be loaded in the {{ content }}
of its parent layout.
Includes
In the layout above, I also have {% include head.html %}
and {% include footer.html %}
. These are known as includes (or partials). Includes are a file that can be inserted into any page. They're perfect for a navbar or footer, where you want to keep the markup consistent throughout the site. Store any includes you use in _includes/
. Here is a portion of my nav.html
code:
site.navigation %}
<li {% if url="" ="link.url" %}class="active" {% endif %}>
<a href="{{ link.url }}" title="{{ link.title }}">{{ link.text }}</a>
</li>
{% endfor %} {%- endraw -%}
Here, I get the URL from the current page (while removing unwanted possible filenames) and store it as url
. Next, I iterate through a list of link dictionaries and build the list in HTML. While iterating through this list, I check if the page URL is the same as the current link in the loop and, if so, set the item's class to active, which then applies a different background color, indicating that the user is on the said page.
Creating Pages
Your homepage needs to be named index.html
(or index.md
if you're writing it in markdown) and be located at the root level of your project. For any other page at the root level, you can create a file such that the title of the file will be the URL to that page. For example, if you create a file at /about.md
, Jekyll will make that available at https://example.com/about.html
(or example.com/about
if you set permalink: /about
in the file's front matter). If you wish to have a page in a subdirectory (or many subdirectories), title that subdirectory what you wish the URL path to be, then drop in an index.html
(or markdown equivalent) at that directory. For example, if I created /this/is/a/test/index.html
, I could access that page at https://example.com/this/is/a/test
.
Check the Jekyll documentation on creating pages.
Writing Posts
All posts live inside the appropriately named _posts
directory.
Files
Each post is its own file, and must have a filename formatted as:
YYYY-MM-DD-title.MARKUP
YYYY-MM-DD
represents the year, month, and day of the post, title
is the title of the post. I always specify this in the front matter of the file, and generally set my own slug and use that for the filename. MARKUP
refers to the file extension and format the file uses. Here are a few examples filenames:
2016-05-29-tech-crew.md
2016-07-04-happy-independence-day.html
Front Matter
Unlike pages, a post must contain front matter. I like to set a more specific date
, as well as title
, slug
, description
, and of course what layout
I'm using.
Indexing Posts
So now you have a bunch of well written and entertaining posts, but how do you show links to them? It's quite easy, you can use Liquid to iterate through your posts to show them in a list:
{%- raw -%}
<ul>
{% for post in site.posts %}
<li>
<a href="{{ post.url }}">{{ post.title }}</a>
</li>
{% endfor %}
</ul>
{%- endraw -%}
If you have a lot of posts, be sure to check out the Pagination plugin for showing your posts on more than one page.
Excerpts
By default, the first paragraph of each post is available at {% raw %}{{ post.excerpt }}
. Alternatively, you can set your own by including the excerpt
variable in your post's front matter. You can configure exceprts even more, such as setting an excerpt_separator
and filtering on the excerpt. Check out post excerpts for more.
Themes
Jekyll supports site-wide themes. If you use the jekyll new
command to create a scaffolded site, it wll use the theme minima
. You can install and swap in other themes by installing the gem
for the theme you are using, and by specifying it in your _config.yml
file. For more information on using and creating a theme package, check out the Jekyll theme docs.
Note, GitHub Pages only supports the minima
theme. If you want to use something else, you must roll your own, bundle the entire source within your project, or use an externally hosted solution.
Plugins and More
GitHub Pages only supports a limited set of plugins. I will cover the ones that I use on this site (most of the ones GitHub supports). View the github-pages gem dependencies for a list on available plugins.
GitHub Pages 404 Page
If you are using GitHub Pages to host your site, you can create your own 404 error page. Instead of showing a visitor of your site a standard GitHub 404 page, you can create your own that fits your site. Note, this can only be done if you use a custom domain, see the GitHub page on Creating a custom 404 page for your GitHub Pages site for more information.
If this applies to you, all you need to do is create a 404.html
or 404.md
file in the project root and add permalink: /404.html
to the front matter, and you're all set!
SASS/SCSS
To add support for SASS/SCSS (which you totally should π), add the following to your _config.yml
file:
sass:
sass_dir: assets/css/_sass
style: compressed
plugins:
- jekyll-sass-converter
Set the sass_dir
to where you store your Sass/SCSS files. You can change the output style to nested
, expanded
, compact
, compressed
, or remove it entirely to keep everything.
Check out the Sass/SCSS docs.
Pagination
Here's what the official docs say about pagination:
With many websites β especially blogs β itβs very common to break the main listing of posts up into smaller lists and display them over multiple pages. Jekyll offers a pagination plugin, so you can automatically generate the appropriate files and folders you need for paginated listings.
So really, why not? To add pagination, add the following to your _config.yml
file:
paginate: 5
paginate_path: /blog/page/:num/
plugins:
- jekyll-paginate
The paginate
number sets how many posts to be included on every page. It makes sense to show many if you only show the title and a link, but fewer if you show the entire contents of each post on one page. The paginate_path
string sets how the pages paths/URLs are formed. Note: the /blog
page cannot have a permalink for pagination to work. To get around this, I make a blog directory, and put my page there: blog.html
β blog/index.html
.
To update your page, simply replace your posts loop to {% for post in paginator.posts %}
. To show the appropriate previous/next buttons, I recommend checking out the Jekyll Pagination Docs.
Syntax Highlighting
Before you can use syntax highlighting, you must add a few things to your _config.yml
file:
highlighter: rouge
kramdown:
syntax_highlighter: rouge
plugins:
- rouge
This sets rouge for syntax highlighting. Jekyll also supports the Pygments highlighter, but GitHub Pages does not, so I will only talk about Rouge in this post. The kramdown:
list configures kramdown, the markdown converter.
In order to use syntax highlighting in your site, you can use the default setting or use GitHub Flavored Markdown's backticks to denote a code block. Default: Use {% highlight python %}
followed by some sweet Python code, closed with {% endhighlight %}
.
When building the site, rouge will seek through the code block and add span tags with classes on parts of the code. These tags can then be styled to have different colors, hence syntax highlighting. Any Rouge or Pygments CSS theme will work. You can generate CSS files from Pygments or by downloading themes, for example, from https://github.com/richleland/pygments-css/.
Search Engine Optimization (SEO)
It is ridiculously easy to get pretty good SEO with Jekyll. The jekyll-seo-tag
plugin does nearly all the heavy lifting to properly make the correct meta tags (description, open graph, Twitter card), and title. All that's really required is to be sure to set some variables in _config.yml
, be sure to set the title
and description
in a page or post's front matter, and include {% seo %}
in the head of your page. Below is what I have added to my _config.yml
file:
plugins:
- jekyll-sitemap
- jekyll-seo-tag
# seo
title: Brian Mitchell
description: Blog posts, projects, social media presence, and more!
url: https://brianm.me
twitter:
username: BrianMitchL
facebook:
app_id:
publisher:
admins:
logo: /assets/images/BM-Logo.png
social:
name: Brian Mitchell
links:
- https://github.com/BrianMitchL
- https://twitter.com/BrianMitchL
google_site_verification: 4-mwXA7aYqZalRm3UuWpPv-aMyFT_zUtA_ks_RK7r5I
The title
, description
, logo
, and url
are used as site wide defaults. Jekyll computes each page's URL for you, so don't worry about setting that for every page. Use twitter
for setting your site Twitter account. This will be used for the twitter:site
field. In a post with a separate author, be sure to set the author
in the front matter to the Twitter username of the author. You can also set the author to be an object with a twitter
field as seen in _data/_authors.yml
. Set the appropriate facebook
fields if you wish to track your shares and page with a facebook application.
The social
section is used for creating a site card on search engines. Enter the name of the website/person/company, followed by a list of other official pages. Set the google_site_verification
string with your Google Webmaster site verification key to have it inserted as a meta tag.
On any page or post, you may define the seo.type
variable. This uses the schema.org types to define what kind of content is on the page.
Feed
Be sure to add jekyll-sitemap
to your _config.yml
.
plugins:
- jekyll-feed
From there all you need to do is add {% feed_meta %}
to the head of your page. This will create a file called sitemap.xml
that will link to every page on your site. This maintains an index of your site that helps search engines find everything.
jemoji - GitHub-flavored Emoji
As usual with plugins, add jemoji
to your _config.yml
file.
plugins:
- jemoji
This plugin will allow you to insert GitHub-flavored Emojis into your pages and posts. For example:
:+1: :heart: :smile:
would produce: π β€οΈ π
Redirect
Add jekyll-redirect-from
to your _config.yml
file.
plugins:
- jekyll-redirect-from
This plugin allows you to give your pages and posts multiple URLs. Redirects are performs by creating new HTML files with an HTTP-REFRESH meta tag pointing to the current page. To use, add the redirect_from
variable to the front matter of a page. It accepts a string, or list of strings. If multiple redirect_from
s are set, only the first one will be used.
redirect_from: /contact
redirect_from:
- /contact
- /other-sites
Travis CI
From what I've found, the best way to test your Jekyll site is to use the gem html-proofer
. This package will look through your build site for HTML errors. To set this up, add the following to your _config.yml
:
exclude: [vendor]
Also add the following to your Gemfile
:
gem 'html-proofer'
This is due to Travis CI bundling gems in the vendor
directory. Next, create a .travis.yml
file and include the following:
sudo: false
language: ruby
cache: bundler
rvm:
- 2.5.1
script:
- bundle exec jekyll build
- bundle exec htmlproofer ./_site --disable-external --assume-extension
env:
global:
- NOKOGIRI_USE_SYSTEM_LIBRARIES=true
The --disable-external
flag disables the checking on 3rd party links. Some websites will block scrapers like the one used in html-proofer
, which will cause the check to fail, so this just skips em' all. The --assume-extension
flag will assume that an href like /about
is the same as about.html
. For more detailed information, check out the Jekyll documentation on continuous integration.
Update 2018-09-02: I have refactored this to use a Rakefile and tell Travis CI to use that (see the .travis.yml). I was having problems with html-proofer matching URLs with a hyphen in them, and needed to use the url_swap
configuration, which I thought was easier to configure in a Rakefile.