I've been using Ghost to blog for some amount of time, but recently I was turned onto Jekyll for blogging with markdown.  More to the point, Jekyll is the backend used by Github Pages blogging.  We can actually use this and a custom DNS entry to make a free scalable blog, documentation portal or website.

Getting Started

First, let's create a root level repo for our blog. It needs to match our org or user id and end in 'github.io'.

In my case, idjohnson:

You'll also want to set it to public if you want people to be able to see it.

At this point, technically we are done with the MVP. There now exists a very basic GH Pages site:


Under settings, we can easily pick from some standard themes…  Here i chose "Midnight Theme"

and when i saved with a bland README.md, it rebuilt and republished the site:

Custom Domains

What about a custom domain?  Maybe something with freshbrewed.science or tpk.pw.

Let's hop over to Azure DNS and add an A Record for tpk.pw:

Here is a CNAME we can use personalblog (personalblog.tpk.pw). Just point that with a CNAME to the existing github.io URL (idjohnson.github.io)

Now let's go back to Github settings for the blog repo:

Let's add the custom DNS entry and save:

we actually see the green published box update with the new URL:

Now sadly this is an HTTP site..

You can go to https but users will get the whole "this cert is bullcrap" warning from google which says, yes it's TLS encrypted, but with an illegit self signed cert (or cert that doesn't match the name): [ note: as we will see in a minute, this is temporary ]

The former URL is still valid

If you wish to enforce SSL (and disable HTTP) then you can enforce that by checking 'enforce https'

I noted that having given Github a beat to verify the DNS (green check), it created an SSL cert for me (thank you Github)

It looks like Github uses LE for that (much as i would do myself):

I certainly did not generate that TLS cert.

We also see that DNS name referenced in two places in our repo.

The first is the _config.yml

title: The TPK Personal Blog
email: isaac.johson@gmail.com
description: >- # this means to ignore newlines until "baseurl:"
  Isaac Johnson Personal GH Blog
url: "https://personalblog.tpk.pw" # the base hostname & protocol for your site, e.g. http://example.com
twitter_username: nulubez
github_username:  idjohnson

The second is a root level CNAME file:

Updating the blog

say we wish to make a post. How hard is it?

Let's checkout a branch and fire up vs code:

$ ls
CNAME  LICENSE  README.md  _config.yml
$ code .
$ git branch
* main
$ git checkout -b feature/firstpost
Switched to a new branch 'feature/firstpost'

Then we can add the file and push it..

~/Workspaces/idjohnson.github.io$ git add FirstPost.md
~/Workspaces/idjohnson.github.io$ git commit -m "My first post"
[feature/firstpost c246d28] My first post
 1 file changed, 19 insertions(+)
 create mode 100644 FirstPost.md
~/Workspaces/idjohnson.github.io$ git push
fatal: The current branch feature/firstpost has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin feature/firstpost

~/Workspaces/idjohnson.github.io$ darf
git push --set-upstream origin feature/firstpost [enter/↑/↓/ctrl+c]
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 1.01 KiB | 1.01 MiB/s, done.
Total 3 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
remote: Create a pull request for 'feature/firstpost' on GitHub by visiting:
remote:      https://github.com/idjohnson/idjohnson.github.io/pull/new/feature/firstpost
To https://github.com/idjohnson/idjohnson.github.io.git
 * [new branch]      feature/firstpost -> feature/firstpost
Branch 'feature/firstpost' set up to track remote branch 'feature/firstpost' from 'origin'.

Nothing appears on our blog at first.  This is because pages is set to a branch spec (in our case "main"):

So let's PR back to main.  

We can go to the PR page where we are prompted to make a PR:

Create the PR

Then if there are no conflicts we can merge right away:

which moves it to merged and closed

I realized i did not organize it right.

I moved to a _posts folder and added a header to the markdown:

note: later i realized the missing part was a lack of "---" before line 1 and after line 3.

Then created and merged a PR

Testing locally

Install Jekyll if you have not already: https://jekyllrb.com/docs/installation/

In fact, getting started with Jekyll locally is as simple as a few steps (documented here)

$ gem install jekyll bundler
# Create a new Jekyll site at ./blog.
$ jekyll new blog
# cd to blog and build
$ cd myblog && bundle exec jekyll serve
# now serving on localhost:4000

We will be looking at the completed repo which is made public here:

You'll want to use `bundle install` and `bundle update` when updating content

~/Workspaces/idjohnson.github.io$ bundle update
Fetching gem metadata from https://rubygems.org/...........
Resolving dependencies...
Using concurrent-ruby 1.1.9
Using minitest 5.14.4
Using thread_safe 0.3.6
Using zeitwerk 2.4.2
Using public_suffix 4.0.6
Using bundler 2.2.26
Using unf_ext
Using eventmachine 1.2.7
Using ffi 1.15.3
Using faraday-em_http 1.0.0
Using faraday-excon 1.1.0
Using faraday-httpclient 1.0.1
Using faraday-net_http_persistent 1.2.0
Using faraday-patron 1.0.0
Using faraday-rack 1.0.0
Using multipart-post 2.1.1
Using forwardable-extended 2.6.0
Using gemoji 3.0.1
Using rb-fsevent 0.11.0
Using rexml 3.2.5
Using liquid 4.0.3
Using mercenary 0.3.6
Using rouge 3.26.0
Using safe_yaml 1.0.5
Using racc 1.5.2
Using jekyll-paginate 1.1.0
Using rubyzip 2.3.2
Using jekyll-swiss 1.0.0
Using unicode-display_width 1.7.0
Using i18n 0.9.5
Using unf 0.1.4
Using tzinfo 1.2.9
Using ethon 0.14.0
Using rb-inotify 0.10.1
Using addressable 2.8.0
Using pathutil 0.16.2
Using kramdown 2.3.1
Using nokogiri 1.12.3 (x86_64-linux)
Using ruby-enum 0.9.0
Using faraday-em_synchrony 1.0.0
Using terminal-table 1.8.0
Using simpleidn 0.2.1
Using typhoeus 1.4.0
Using sass-listen 4.0.0
Using listen 3.7.0
Using kramdown-parser-gfm 1.1.0
Using commonmarker 0.17.13
Using dnsruby 1.61.7
Using sass 3.7.4
Using jekyll-watch 2.2.1
Using jekyll-sass-converter 1.5.2
Using execjs 2.8.1
Using faraday-net_http 1.0.1
Using activesupport
Using colorator 1.1.0
Using html-pipeline 2.14.0
Using http_parser.rb 0.6.0
Using ruby2_keywords 0.0.5
Using em-websocket 0.5.2
Using coffee-script-source 1.11.1
Using jekyll 3.9.0
Using coffee-script 2.4.1
Using jekyll-avatar 0.7.0
Using jekyll-redirect-from 0.16.0
Using jekyll-relative-links 0.6.1
Using jekyll-remote-theme 0.4.3
Using jekyll-seo-tag 2.7.1
Using jekyll-sitemap 1.4.0
Using jekyll-titles-from-headings 0.5.3
Using jemoji 0.12.0
Using jekyll-coffeescript 1.1.1
Using jekyll-theme-architect 0.2.0
Using jekyll-theme-cayman 0.2.0
Using jekyll-theme-dinky 0.2.0
Using jekyll-theme-hacker 0.2.0
Using jekyll-theme-leap-day 0.2.0
Using jekyll-theme-merlot 0.2.0
Using jekyll-theme-midnight 0.2.0
Using jekyll-theme-minimal 0.2.0
Using jekyll-theme-modernist 0.2.0
Using jekyll-theme-slate 0.2.0
Using jekyll-theme-tactile 0.2.0
Using jekyll-theme-time-machine 0.2.0
Using faraday 1.7.0
Using jekyll-commonmark 1.3.1
Using jekyll-default-layout 0.1.4
Using jekyll-commonmark-ghpages 0.1.6
Using jekyll-mentions 1.6.0
Using jekyll-optional-front-matter 0.3.2
Using jekyll-readme-index 0.3.0
Using sawyer 0.8.2
Using jekyll-feed 0.15.1
Using octokit 4.21.0
Using minima 2.5.1
Using jekyll-gist 1.5.0
Using jekyll-github-metadata 2.13.0
Fetching github-pages-health-check 1.17.7 (was 1.17.2)
Using jekyll-theme-primer 0.6.0
Installing github-pages-health-check 1.17.7 (was 1.17.2)
Fetching github-pages 219 (was 218)
Installing github-pages 219 (was 218)
Bundle updated!

Next to serve this locally, you can use `bundle exec jekyll serve`

~/Workspaces/idjohnson.github.io$ bundle exec jekyll serve
Configuration file: /home/builder/Workspaces/idjohnson.github.io/_config.yml
            Source: /home/builder/Workspaces/idjohnson.github.io
       Destination: /home/builder/Workspaces/idjohnson.github.io/_site
 Incremental build: disabled. Enable with --incremental
      Remote Theme: Using theme pages-themes/midnight
       Jekyll Feed: Generating feed for posts
   GitHub Metadata: No GitHub API authentication could be found. Some fields may be missing or have incorrect data.
                    done in 1.619 seconds.
/home/builder/gems/gems/pathutil-0.16.2/lib/pathutil.rb:502: warning: Using the last argument as keyword parameters is deprecated
                    Auto-regeneration may not work on some Windows versions.
                    Please see: https://github.com/Microsoft/BashOnWindows/issues/216
                    If it does not work, please upgrade Bash on Windows or run Jekyll with --no-watch.
 Auto-regeneration: enabled for '/home/builder/Workspaces/idjohnson.github.io'
    Server address:
  Server running... press ctrl-c to stop.
[2021-08-25 13:52:31] ERROR `/favicon.ico' not found.

The key things you'll note are that posts are automatically added from _posts directories.  Note: the underscore is important there.

And we can see that matches here:

The other important note is that the markdown really must be prefixed with a date string.  Also, in the markdown itself, we need a YAML block at the top *WITH* delimiters.  Categories are basically tags.


layout: post
title:  "First Post"
date:   2021-08-23 11:10:43 -0500
categories: personal

## First Post

So now we have a blog. What's next?  What content do you want to share?

The other thing you'll note is that we can serve pages from more than one folder.

Here i have a "/docs" root with a post: https://github.com/idjohnson/idjohnson.github.io/blob/feature/firstpost/docs/_posts/2021-08-23-test2.md which is served just fine.

and of course the other two in a _posts folder:


Adding pages

Say that I wanted to add a page.  

First, it's important that *if* you move off the default theme, you create _layouts for any top level type. E.g. page:

Then we can create a page.  Here i will add it directly under docs:

and that permalink is now served as http://localhost:4000/wassup/

We can now add and push it to Github:

`~/Workspaces/idjohnson.github.io$ git status
On branch feature/firstpost
Your branch is up to date with 'origin/feature/firstpost'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)

nothing added to commit but untracked files present (use "git add" to track)
~/Workspaces/idjohnson.github.io$ git add docs/
~/Workspaces/idjohnson.github.io$ git commit -m "add a wassup sample page"
[feature/firstpost 0f639e0] add a wassup sample page
 1 file changed, 6 insertions(+)
 create mode 100644 docs/sample.md

~/Workspaces/idjohnson.github.io$ git push
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 8 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 435 bytes | 435.00 KiB/s, done.
Total 4 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://github.com/idjohnson/idjohnson.github.io.git
   b455bdd..0f639e0  feature/firstpost -> feature/firstpost

However, when i look, i of course don't see it live;

This is because we don't serve out of the feature/firstpost branch.. we need to merge to main to make it live.

We just need to make a PR:

Then merge it to make it live


after a minute or so (hey, it's free) we see the pipeline ran and our blog is rendered and live

This includes pages

As an aside, on some browsers i needed to add 'index.html' to the path.

Cleaning up.

So now that we have a sample blog. having a bunch of "hello world" posts won't be of much use.

I'll first create a new branch off main (now that its merged, it should have more content)

~/Workspaces/idjohnson.github.io$ git checkout main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
~/Workspaces/idjohnson.github.io$ git pull
remote: Enumerating objects: 2, done.
remote: Counting objects: 100% (2/2), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 2 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (2/2), 1.20 KiB | 1.20 MiB/s, done.
From https://github.com/idjohnson/idjohnson.github.io
   fa5fb09..0365008  main       -> origin/main
Updating fa5fb09..0365008
 .gitignore                                     |   5 ++
 404.html                                       |  25 ++++++
 Gemfile                                        |  32 ++++++++
 Gemfile.lock                                   | 282 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 _config.yml                                    |  55 ++++++++++++-
 _layouts/home.html                             |   4 +
 _layouts/page.html                             |   4 +
 _layouts/post.html                             |   4 +
 FirstPost.md => _posts/2021-08-23-firstpost.md |   9 ++-
 _posts/2021-08-23-welcome-to-jekyll.markdown   |  29 +++++++
 about.markdown                                 |  18 +++++
 docs/_posts/2021-08-23-test2.md                |   7 ++
 docs/sample.md                                 |   6 ++
 index.markdown                                 |  15 ++++
 14 files changed, 493 insertions(+), 2 deletions(-)
 create mode 100644 .gitignore
 create mode 100644 404.html
 create mode 100644 Gemfile
 create mode 100644 Gemfile.lock
 create mode 100644 _layouts/home.html
 create mode 100644 _layouts/page.html
 create mode 100644 _layouts/post.html
 rename FirstPost.md => _posts/2021-08-23-firstpost.md (92%)
 create mode 100644 _posts/2021-08-23-welcome-to-jekyll.markdown
 create mode 100644 about.markdown
 create mode 100644 docs/_posts/2021-08-23-test2.md
 create mode 100644 docs/sample.md
 create mode 100644 index.markdown
~/Workspaces/idjohnson.github.io$ git checkout -b feature/cleanup-blog
Switched to a new branch 'feature/cleanup-blog'

First, in /index.markdown let's create an IF block for a new field we will use called 'visible'

# Feel free to add content and custom Front Matter to this file.
# To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults

layout: home
My posts:
  {% for post in site.posts %}
  {% if post.visible== 1  %}
      <a href="{{ post.url }}">{{ post.title }}</a>
      {{ post.excerpt }}
  {% endif %}
  {% endfor %}

Next, in our posts, set the firstpost to visible:

layout: post
title:  "First Post"
date:   2021-08-23 11:10:43 -0500
categories: personal
visible: 1
## First Post

and the rest use "visible: 0"

now serve locally:

But say you want to avoid even that.  You could add a line in the post.html to also mask it (though note the variable is under "page" not "post")

layout: default
{% if page.visible==1 %}
{{ content }}
{% endif %}

For instance, maybe i want to just highly draft posts are just that - drafts.

layout: default
{% if page.visible!=1 %}
<h1><font color=red>**DRAFT POST: MAY BE INCOMPLETE**</font></h1>
{% endif %}
{{ content }}

and those marked visible: 0 will show that

I found a few guides, but the solution was close but not in any of them.

You'll want to create an assets/css/style.scss which adds to the existing style sheet.

Then you can set the header to visibility of none:


@import "{{ site.theme }}";
#header {
  display: none !important;
.btn {
  display: none !important;

I then pushed, created a PR and completed it to make it live:


In this article we started by creating a new Github repo with our username/org and then setting up a basic blog with Github pages.  We then setup Jekyll with a new theme and showed how to post blog posts and pages.  

We then added a personal DNS entry and explored local testing.  Lastly we updated our templates to hide future/deprecated posts with a visibility flag and hide the "view on Github" link header for pages that might not need it.