This is my first post! It seems fitting that my first post should be about how to build a blog like this one.
cozy.computer is built using:
Jekyll is a simple tool for building a static website
.
A static website
is one that is delivered to your browser exactly as it is stored. When you visit a static web page, you will always receive the same page no matter who you are and no matter where you’ve come from.
A dynamic website
is the opposite - the pages you visit are generated by the server.
Static websites
are useful because they can be easy to set up and host. You don’t have to configure and run a server (there’s still a server, you just don’t need to set it up), don’t have to deal with modeling your content, don’t have to setup a database, don’t have to figure out caching. Just give the files to your host and have the host serve them when they are requested.
Static websites
are the foundational websites of the internet. Many early webpages were just static files in a folder served from a server. As websites became more complex,dynamic websites
allowed for serving different users different content, allowed users to create their own content, allowed management of user sessions, storage and retrieval of content from databases and memory stores, and more.
Although they’re old fashioned, static website generators are still hugely popular and are used by many companies including Nest, MailChimp, and Vox Media.
One reason for this is cache invalidation
- knowing when cached content needs to be refreshed. Because of the large number of dynamic elements in a modern web page, it can be difficult to determine if its content needs to be updated or not. Caches of web pages are used to save network traffic and delivery time: cached pages can be saved in the browser or delivered via a Content Delivery Network (CDN)
- a network of cache servers where the nearest server can quickly deliver you cached content. Static websites
have a simpler caching protocol - if the file has been updated, update the cache.
Static website generators typically:
markdown
, text
, or HTML
files.templates
, where content can be programmatically inserted into a page when it is generated using a templating language
. For instance, a post template might have the post title, date, and content programmatically inserted from the markdown file for that post.partials
- small templates that can be used in other pages. For instance, a footer template is a partial that can be included in other pages.Static website generators build all template
content into a set of static resources (HTML, Javascript, CSS, assets like images and video, etc). These static resources can then be hosted and served on the internet.
Jekyll is probably the most popular static website generator. It uses a templating language called Liquid, supports content written in markdown
, and supports Sass CSS preprocessing. It ships with a local development server with hot reloading. Jekyll is used in the background when you host static websites on Github Pages. Some complain that Jekyll is slow to build for larger websites.
Hugo bills itself as the “fastest framework”, meaning it can build much faster than Jekyll. It uses a templating engine based on Go. Hugo also supports content written in markdown
and has a local development server.
Gatsby is a static website generator that uses modern web technologies like React, GraphQL, and Webpack to generate a Progressive Web App. It allows for developing a more complicated website with modern features.
I’ve chosen Jekyll because of recommendations from friends at the Recurse Center, because I am only interested in writing simple markdown posts, and because I intend to host on Github Pages which is already set up to work with Jekyll.
Static websites are just a collection of files and there are many ways to host them. I’ve chosen Github Pages.
Github Pages lets you host one personal website and unlimited project websites directly from a Github repository for free. It has built-in support for Jekyll and simple setup for HTTPS and custom domain names. I’ve chosen it for these reasons and because I already planned to keep my blog on Github for version control.
Some other free options (depending on usage tiers):
netlify.com
subdomain or with a custom domain. Netlify advertises global CDN (the global cache network) and instant cache invalidation.web.app
or firebaseapp.com
subdomain or with custom domain. Firebase advertises global CDN, A/B testing, and analytics.Github Pages allows you to serve the contents of your Github repository as a website.
By default visitors can access your personal website at your_username.github.io
or your_organization_name.github.io
and your project websites at your_username.github.io/project_repository_name
. You can also set up a custom domain name if you have one or want to purchase one.
Steps to setting up Github Pages for a personal website:
your_username.github.io
or your_custom_domain_name
. I named my repository cozy_computer
.index.md
and _config.yml
in your repository.You should now be able to visit your static website at your_username.github.io
but not yet at your custom domain.
Entering the custom domain name above will create a CNAME
file in your repository. CNAME
records - Canonical Name records - are used to provide aliases to domain names. They allow you to say, for instance, that www.cozy.computer
and cozy.computer
are the same website.
A
records map domain names to IP addresses. They’re how you get from a domain (ex cozy.computer
) to the actual machine hosting the static website.
In my CNAME
file is cozy.computer
. This tells visitors to merklebros.github.io
(my default personal website) and visitors to www.cozy.computer
to look for cozy.computer
when using A
records to look up the IP address where my website is hosted.
When you purchase a domain name, the A
records are set to point to the DNS provider’s (or affiliate’s) IP addresses. Visitors to your domain might be served an advertisement for the DNS provider or a 404 page.
You can update the A
records to point to Github’s servers so that Github Pages can serve your static website instead. Most providers will have a guide for how to do this.
Log in to your DNS provider - mine was namecheap.com
- and create four A
records that point your domain at the Github servers. Check here to see what IP addresses to point to. Currently they are:
185.199.108.153
185.199.109.153
185.199.110.153
185.199.111.153
By default these probably only redirect requests to your apex domain
- a custom domain that does not contain a subdomain (ex example.com
).
To redirect the www
subdomain (ex www.example.com
), add a CNAME
record at your DNS provider with HOST
set to www
and with a value of your_username.github.io.
. Note the .
at the end of the domain.
When the changes have propagated (maybe up to 24 hours but typically under 1 hour) you can visit your static website at your custom domain.
To develop your static website locally using Jekyll first install the following dependencies:
Ruby >= 2.4.0
, the Ruby programming languageRubyGems
, the Ruby package managerGCC
, GNU Compiler CollectionMake
. a GNU build toolThen install Jekyll
and bundler
gems:
gem install jekyll bundler
Clone your repository from Github and cd
into it. Remove any files except for the CNAME
file. To set up a new Jekyll project, run:
jekyll new . --force
where jekyll new
sets up a new Jekyll project, .
in the current directory, --force
even if the path already exists (and the current directory does already exist). Start a development server with hot reload activated with bundle exec jekyll serve -l
then open http://localhost:4000.
The default files created by jekyll new
are:
Jekyll project directory
- _config.yml
- Gemfile
- Gemfile.lock
- .gitignore
- index.markdown
- about.markdown
- 404.html
- _posts/
- _site/
_config.yml
is a YAML (recursive acronym for YAML A'int Markdown Lanaguage)
file for configuring Jekyll. You can fill in the details for your website’s title, email, description (the text that will show up in a Google search), url. You can specify any key: value
pair here and you will be able to access it in the other files for your website.
My slightly modified _config.yml
is below, I’ve removed theme: minima
because I like the Github Pages default theme, Primer
. I’ve added a custom key: value
pair, average_reading_speed_words_per_minute: 250
so that I can show a read time on my posts later.
title: Cozy Computer
author: Patrick McCarver
email: patrick@cozy.computer
average_reading_speed_words_per_minute: 250
description: >-
Be cozy and learn things with me (Patrick McCarver)
baseurl: ""
url: "https://cozy.computer"
twitter_username: cozy_computer
github_username: Merklebros
permalink: /:title
plugins:
- jekyll-feed
Jekyll allows you to specify the path for your posts and pages with the permalink key. This is how they will show up in the address bar of your web page. The default path is long permalink: /:categories/:year/:month/:day/:title:output_ext
. I prefer to just show the title so set permalink: /:title
, so all pages are available as https://cozy.computer/post_title
.
Jekyll has many plugins. The default plugin jekll-feed will create an Atom RSS feed at /feed.xml
.
A Gemfile
is a file describing dependencies for Ruby programs. These are used with Bundler
, a dependency management program for Ruby projects that uses the Gemfile
to install Ruby gems and pin
them to specific versions with Gemfile.lock
. Gemfile
is analogous to Python requirements.txt
or NodeJS package.json
and package-lock.json
.
When hosting on Github Pages, you can run into dependency issues because Github Pages rebuilds your website using their version of Jekyll before deploying it. This can lead to unexpected behavior. For instance, different versions of Ruby parse YAML
files differently. To ensure that the deployed website behaves the same as the local development build, use the github-pages gem which sets up local dependencies that match Github Pages as closely as possible.
My Gemfile
is below. I’ve cleaned up some comments and followed the instructions for setting up the github-pages
gem by removing the jekyll
gem and uncommenting gem "github-pages", group: :jekyll_plugins
. I’ve also removed the minima
gem since I’m not using that theme.
source "https://rubygems.org"
gem "github-pages", group: :jekyll_plugins
group :jekyll_plugins do
gem "jekyll-feed", "~> 0.12"
end
# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
# and associated library.
install_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do
gem "tzinfo", "~> 1.2"
gem "tzinfo-data"
end
# Performance-booster for watching directories on Windows
gem "wdm", "~> 0.1.1", :install_if => Gem.win_platform?
Run bundle install
to install the dependencies specific to the github-pages
gem.
A .gitignore
is generated by jekyll new
. It tells Git to ignore files related to the Jekyll build process: _site, .sass-cache, .jekyll-cache, .jekyll-metadata, vendor
.
Jekyll generates two .markdown
files that can be safely renamed to .md
instead.
Markdown files are the content for your website. You can write post and page content using Markdown and then include them into HTML layouts.
At the top of every page you can optionally include YAML
key-value pairs to use in the document. Jekyll calls this Front Matter
.
In index.md
, the layout is set to home
but the layout file is hidden from view by the default theme so we can’t see what home.html
looks like. This is strangely by design to ease the user experience. Remove layout: home
and type a little poem along with a list of projects and blog posts for the home page. Here’s my index.md
:
Cozy computer is CC to me
Spindled in comfort and beeping softly
Cozy computer with cuppa coffee
Or mocha, or Hi-C, or even Capri
Cozy computer (CC) whispers squeaky
Look upon my works ye mighty, and drink tea
## My works
[a 404 work](work.md)
## Blog
{% for post in site.posts %}
[{{post.title}}]({{post.url}})
{% endfor %}
We can see that in Markdown you can link to other Markdown files, and you can use the Liquid
templating language to insert content. The last three lines take every post (accessible under site.posts
) and make a link for each post.
Jekyll supports custom layouts and looks for them in the _layouts
folder. Create this folder and create default.html
there. Jekyll will use a default
layout for index.md
without having to specify it in the Front Matter. I’m using a slightly modified default.html
from the Primer theme available from their Github repo.
<!DOCTYPE html>
<html lang="{{ site.lang | default: "en-US" }}">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% seo %}
<link
rel="stylesheet"
href="{{ "/assets/css/style.css?v="
| append: site.github.build_revision
| relative_url }}"
>
</head>
<body>
<div class="container-lg px-3 my-5 markdown-body">
<h2><a href="{{ "/" | absolute_url }}">{{ site.title }}</a></h2>
{% include contact_navigation.html %}
{{ content }}
{% include footer.html %}
</div>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/anchor-js/4.1.0/anchor.min.js"
integrity="sha256-lZaRhKri35AyJSypXXs4o6OPFTbTmUoltBbDCbdzegg="
crossorigin="anonymous">
</script>
<script>anchors.add();</script>
{% if site.google_analytics %}
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '{{ site.google_analytics }}', 'auto');
ga('send', 'pageview');
</script>
{% endif %}
</body>
</html>
The default.html
is a template for every page. Place the basic structures of your website that will be present on all pages here. Liquid templates are either {% do stuff %}
or {{ variable }}
depending on whether they have logic in them or just reference a variable. The {% seo %}
adds some metadata to the page. In the body I dynamically include contact_navigation.html
- a social media navigation bar that I want on every page, content
which is the contents of whatever markdown file is given to this template, and footer.html
, the site footer.
The include
keyword is used to add contact_navigation.html
and footer.html
because they are partials
- small snippets of HTML you can compose in other templates. Jekyll looks in the _include
folder for partials you include. Create the _include
folder and inside create contact_navigation.html
:
<p id="contact-navigation">
<a href="mailto:{{site.email}}">
<img src="assets/icons8-email-32.png" alt="Email logo">
</a>
<a href="https://twitter.com/{{site.twitter_username}}">
<img src="assets/twitter_logo_blue_small.png" alt="Twitter logo">
</a>
<a href="https://github.com/{{site.github_username}}">
<img src="assets/github_mark_small.png" alt="Github Mark logo">
</a>
<——- Find me (Patrick McCarver)
</p>
The contact navigation bar uses images that I’ve saved in /assets
but you can save them wherever you want. You can access site variables from _config.yml
using site.variable_name
, ex site.email
, site.twitter_username
, etc.
Inside _include/footer.html
:
<div class="footer mt-5 pt-3 text-left text-gray">
<p>© {{site.author}}</p>
<p>
<img src="assets/recurse_logo_small.png" alt="Recurse Center Logo">
<---- Want to become a better programmer?
<a id="recurse-center-link" href="https://www.recurse.com">
Join the Recurse Center!
</a>
</p>
<p>Email icon by
<a href="https://icons8.com" alt="https://icons8.com">Icons8</a>
</p>
</div>
The scaffold for the home page is finished! Let’s create one for posts in _layouts/post.html
.
---
layout: default
---
<h1>{{page.title}}
{% assign word_count = content | number_of_words %}
<span id="post-word-count">
{{word_count}} words
({{
word_count |
divided_by: site.average_reading_speed_words_per_minute |
plus: 1 |
round
}} min)
<span>
</h1>
{{ content }}
I want posts to look the same as the home page but with different content, so I use the default
layout. I’ll display the page’s title
, word count, read time, and content. Liquid template language uses |
to pass variables to Liquid internal functions, like in Bash
. In {% assign word_count = content | number_of_words %}
, content is passed to number_of_words
, an internal liquid function for calculating number of words. Below that I do my own math to display the reading time. Like before, content
is the content of the Markdown file we pass to this template.
There’s a 404 page you can customize but I just leave it alone.
Jekyll looks for posts in _posts
and drafts in _drafts
. Posts are markdown files that must be named with the date leading: YYYY-MM-DD-title-of-post.md
.
You can include Front Matter YAML
at the beginning of the post to use in your templates. Jekyll supports assigning categories
and tags
to pages and posts. Categories
are meant to be separate sections of the website and get their own url string with the category name in it: your_domain.xyz/category/blah
. Tags
are used internally so that you can find all pages or posts with that tag. Below is the Front Matter for this post:
---
layout: post
title: "Setting up a personal blog using Jekyll, Github Pages, and a custom domain name"
date: 2020-02-17
---
The rest of the post goes below the Front Matter and will be inserted as content
into the post template.
Jekyll builds your static website locally in _site/
. Depending on your deployment you may or may not need the built website.
If you deploy to Github Pages, it will rebuild your website for you, so you don’t need to push _site/
to your repository.
If you deploy to a hosting platform that does not build your website with Jekyll on their server, you will need to put _site/
there.
As I’m hosting with Github Pages, I’m free to push my repository as-is and Github Pages will build it for me.
That’s it! To continue building the website just update pages or posts and push the changes to Github and the site will be continually redeployed.
A fun note: While writing this post Liquid kept trying to interpret my quoted code blocks with {{}}
and {% %}
in them as Liquid templates instead of plain text. To get around this, you can wrap the entire text in raw
tags as discused here.