New Jekyll Website

This is a brief post informing the reader of my new online portfolio website and some details about the use of static site generation.

This article is expected to be about a 3 minute read.

Table of Contents

Jekyll

Background

For a while now, I had planned to create my own website to document various projects I have involved myself with over the years. I have always been interested in automation and efficiency (more so as they can be applied to network security in recent years), so when I stumbled across the Jekyll framework, I knew it was time.

Jekyll is a static site generator powered by the Ruby programming language that uses RubyGems to bundle packages. Webpages can be completely written in .markdown formatting (embedded HTML is permitted) and are generated into HTML by “building” (jekyll build) the website from the markdown sources. The project is also the engine behind GitHub Pages, proving it favorable for fast and technical bloggers already familiar with the .md syntaxes.

Setup

Since I already had Apache2 installed and configured to my liking with a Let’s Encrypt TLS certificate, all I had to do was install Ruby and some other prerequisites to generate the initial Jekyll project bundle:

# Ubuntu 16.04
sudo apt-get install ruby-full build-essential zlib1g-dev
echo '# Install RubyGems to ~/gems' >> ~/.bashrc
echo 'export GEM_HOME="$HOME/gems"' >> ~/.bashrc
echo 'export PATH="$HOME/gems/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
gem install jekyll bundler
jekyll new jekyll_website
cd jekyll_website/
bundle exec jekyll serve

The software includes a jekyll serve feature to quickly build and test the website in a “dev-like” environment (default set to http://localhost:4000) which I personally have no use for since I can just modify iptables to only allow connections from my IP address to ports 80/443 when testing changes for production:

iptables -A INPUT -s <ip> -p tcp -m multiport --dports 80,443 -j ACCEPT
iptables -A INPUT -j DROP

The only tasks left to complete post-installation involve importing (or creating) a custom Jekyll theme and adding basic navigation to other pages. I opted to place all blog-like posts (e.g. this post) in ./_posts/ and my main pages in ./, the new document root for my website. These are listed in my _config.yml file:

# Info
title: "Andrew Burch"
email: "drew@andrewburch.me"
description: "Continuously updated Jekyll portfolio website documenting educational background, professional achievements and personal projects."
timezone: "America/Detroit"
locale: "en-US"

# Extra pages
collections:
  projects:
    output: true

# Serving
host: andrewburch.me
port: 4000

# Build settings
theme: texture
show_excerpts: true
texture:
  title: "Andrew Burch"
  tagline: "Senior Security Engineer"
  date_format: "%b %-d, %Y"
  social_links:
    linkedIn: "in/andrew-burch-me"
    mail: "drew@andrewburch.me"
  style: black
  showPicker: false
  showNav: true
  navigation:
    - title: "Home"
      url: "/"
    - title: "About"
      url: "/about/"
    - title: "Projects"
      url: "/projects/"
    - title: "Contact"
      url: "/contact/"
    - title: "PGP"
      url: "/pgp/"
    - title: "Résumé"
      url: "/resume/"
plugins:
  - jekyll-email-protect
  - jekyll-feed
  - jekyll-minifier
  - jekyll-toc
  - jemoji
  - liquid_reading_time
  - nokogiri
  - premonition

# Minifier
jekyll-minifier:
  compress_css: true                # Default: true
  compress_javascript: true         # Default: true
  compress_js_templates: false      # Default: false
  compress_json: true               # Default: true
  preserve_line_breaks: true        # Default: false
  preserve_patterns:                # Default: (empty)
  preserve_php: true                # Default: false
  remove_comments: true             # Default: true
  remove_form_attributes: false     # Default: false
  remove_http_protocol: false       # Default: false
  remove_https_protocol: false      # Default: false
  remove_input_attributes: false    # Default: false
  remove_intertag_spaces: false     # Default: false
  remove_javascript_protocol: false # Default: false
  remove_link_attributes: false     # Default: false
  remove_multi_spaces: true         # Default: true
  remove_quotes: false              # Default: false
  remove_script_attributes: false   # Default: false
  remove_spaces_inside_tags: true   # Default: true
  remove_style_attributes: false    # Default: false
  simple_boolean_attributes: false  # Default: false
  simple_doctype: false             # Default: false
  uglifier_args:                    # Default: (empty)
    harmony: true

# TOC
toc:
  item_class: toc-entry
  item_prefix: toc-
  list_class: section-nav
  list_id: toc
  max_level: 6
  min_level: 1
  no_toc_section_class: no_toc_section
  ordered_list: false
  sublist_class: ''

For my contact page, I used a free forms API called Formspree which forwards submissions to my personal email address.

<div id="contact">
  <form action="https://formspree.io/f/mnqogbwp" method="POST">
    <label for="name">Name</label>
    <input type="text" id="name" name="name" class="full-width"><br>
    <br>
    <label for="email">Email Address</label>
    <input type="email" id="email" name="_replyto" class="full-width"><br>
    <br>
    <label for="message">Message</label>
    <textarea name="message" id="message" cols="30" rows="10" class="full-width"></textarea><br>
    <br>
    <input type="submit" value="Send" class="button-outline"><br>
    <br>
  </form>
</div>

Finally, I decided to use the “texture” theme provided by samarsault due to its elegant and minimalistic design. I was in need of additional font characters for the social link icons in the navigation header (GitHub, Twitter, Mail etc.) so I did edit the following (.eot, .svg, .ttf, .woff, .woff2 updates available here):

~/.rvm/gems/ruby-2.6.5/gems/texture-0.5/_sass/ext/_fonts.scss
~/.rvm/gems/ruby-2.6.5/gems/texture-0.5/_includes/page_header.html
~/.rvm/gems/ruby-2.6.5/gems/texture-0.5/assets/font/*

I intend to continue tweaking the theme as I see fit, but for now, it gets the job done. For more information, I recommend visiting the Jekyll website and browsing the MIT-licensed themes available at jekyllthemes.org.


Changelog

Update: Jun 6, 2021

A custom toggleable dark theme has been added by clicking the crescent moon at the top of the page, to the left of the “Home” navigation bar link and utilizes localStorage to save user preference across the site. JavaScript and CSS rules follow below.

if(window.localStorage.getItem('dark') === 'true') {
  toggleDarkMode();
}
function toggleDarkMode() {
  var id = 'dark_theme';
  var element = document.getElementById(id);
  var dark = window.localStorage.getItem('dark');
  var head = document.getElementsByTagName('head')[0];
  if(!(element)) {
    var link = document.createElement('link');
    link.id = id;
    link.rel = 'stylesheet';
    link.type = 'text/css';
    link.href = 'https://andrewburch.me/assets/css/dark_theme.css';
    link.media = 'all';
    head.appendChild(link);
    window.localStorage.setItem('dark', 'true');
  } else {
    head.removeChild(element);
    window.localStorage.setItem('dark', 'false');
  }
}
<button id="dark_theme_button" onclick="toggleDarkMode();"></button>
body {
  background-color: rgb(50, 50, 50);
}

main {
  background-color: rgb(50, 50, 50);
  color: lightgray !important;
}

pre.highlight, code {
  background-color: rgb(70, 70, 70);
  color: #bfbfbf;
}

.highlight {
  background-color: rgb(70, 70, 70);
}

blockquote {
  border-left: .25rem solid gray;
}

input[type=text], input[type=email], textarea {
  color: rgb(20, 20, 20);
}

hr {
  background-color: lightgray;
  border-color: lightgray;
  color: lightgray;
}

.post-title {
  color: rgb(110, 110, 110);
}

.progressbar > div {
  color: black;
}

.section-nav {
  border-color: gray;
}

.section-nav,
.premonition {
  background-color: rgb(70, 70, 70) !important;
}

.section-nav,
.section-nav a,
.premonition .content,
.premonition.citation a {
  color: #bfbfbf !important;
}

.premonition code {
  background-color: rgb(90, 90, 90);
}

.premonition.pn-quote {
  background-image: url("data:image/svg+xml,%3C%3Fxml%20version='1.0'%20encoding='UTF-8'%3F%3E%3Csvg%20width='165px'%20height='165px'%20viewBox='0%200%20165%20165'%20version='1.1'%20xmlns='http://www.w3.org/2000/svg'%20xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg%20id='Page-1'%20stroke='none'%20stroke-width='1'%20fill='none'%20fill-rule='evenodd'%3E%3Cpath%20d='M104.546838,164.525333%20C97.1871585,164.350607%2090.6368822,160.915227%2090.6512001,150.013018%20C90.4479076,131.842639%2090.4697154,98.303237%2090.6512001,49.7828789%20C91.9844555,2.36817118%20138.064959,0.504907944%20148.576644,0.0692731383%20C152.479575,0.302510658%20153.780675,2.21617827%20154.578947,4.17356105%20C155.831948,9.88458567%20155.831948,17.6357453%20154.578947,27.4270401%20C153.93686,32.7057192%20151.936092,35.3224781%20148.576644,35.2773166%20C143.472082,35.2236794%20151.862467,35.2263624%20140.927765,35.2773166%20C128.559674,35.7091823%20122.660334,39.3672244%20122.615074,56.9085817%20C122.635604,63.1213926%20122.635604,71.5842998%20122.615074,82.2973033%20C138.48496,82.4101196%20149.139584,82.4488979%20154.578947,82.4136382%20C159.435737,82.5353733%20163.923774,84.3352392%20164.565789,96.288498%20C164.874062,119.857257%20164.829662,136.387115%20164.782895,150.013018%20C164.664253,157.17723%20161.233392,164.356416%20151.753558,164.525333%20C127.51005,164.615729%20113.455097,164.525333%20104.546838,164.525333%20Z%20M14.0400451,164.45606%20C6.68036548,164.281334%200.130089247,160.845954%200.144407166,149.943745%20C-0.058885353,131.773366%20-0.0370775896,98.2339638%200.144407166,49.7136058%20C1.47766255,2.29889804%2047.5581663,0.435634806%2058.0698511,-9.9475983e-14%20C61.9727821,0.233237519%2063.2738816,2.14690514%2064.0721544,4.10428791%20C65.3251551,9.81531253%2065.3251551,17.5664722%2064.0721544,27.3577669%20C63.4300669,32.6364461%2061.4292991,35.2532049%2058.0698511,35.2080434%20C52.9652887,35.1544062%2061.3556736,35.1570892%2050.4209719,35.2080434%20C38.0528815,35.6399092%2032.153541,39.2979513%2032.1082808,56.8393085%20C32.1288111,63.0521194%2032.1288111,71.5150266%2032.1082808,82.2280302%20C47.9781667,82.3408464%2058.6327912,82.3796247%2064.0721544,82.3443651%20C68.9289443,82.4661002%2073.4169814,84.265966%2074.0589965,96.2192249%20C74.367269,119.787984%2074.3228688,136.317842%2074.2761018,149.943745%20C74.1574604,157.107957%2070.7265987,164.287143%2061.2467647,164.45606%20C37.0032571,164.546456%2022.9483044,164.45606%2014.0400451,164.45606%20Z'%20id='Quote'%20fill='%23bfbfbf'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}

.decorator::before {
  background-color: lightgray !important;
}

.details header {
  color: lightgray !important;
}

button {
  border-style: none;
  height: 50px;
  margin: auto;
  width: 50px;
}
button:hover {
  filter: brightness(0.5);
}
#dark_theme_button {
  background: url('https://andrewburch.me/assets/img/dark_theme_icon.png') no-repeat center center;
}

Update: Jul 11, 2021

After the initial setup of Jekyll, I decided to write a few scripts to automate updates, the build process, and synchronization between separate versions of Ruby.

build.sh

#!/bin/bash

# Ruby
rsync -aPv --delete --inplace ~/.rvm/gems/ruby-2.6.5/gems/texture-0.5 ~/.rvm/gems/ruby-2.7.0/gems/texture-0.5/

# Website
cd ~/website/jekyll_website
dos2unix ./*.md
dos2unix ./_posts/*.md
dos2unix ./_posts_templates/*.md
dos2unix ./_projects/*.md
dos2unix ./_projects_templates/*.md
export JEKYLL_ENV="production"
bundle exec jekyll build -V

# Post build processing
cd ~/website/jekyll_website/_site
find . -type f -name '*.html' -print0 | xargs -0 dos2unix
find ./assets/ -type f -not -name '*.svg' -exec mat2 --inplace -V {} +

# Tor
cd ~/website/tor
rsync -aPv --delete --inplace ~/website/jekyll_website/_site ./
website="https://andrewburch.me/"
tor="http://"$(sudo cat /var/lib/tor/hidden_service/hostname)"/"
find . -type f \( -name '*.html' -o -name '*.css' -o -name '*.js' \) \
  -exec rpl -v "$website" "$tor" {} +

# Links
echo "Website: $website"
echo "Tor: $tor"

flush.sh

#!/bin/bash

cd ~/website/jekyll_website
rm -frv ./_includes/
rm -frv ./_layouts/
rm -frv ./_posts/
rm -frv ./_posts_templates/
rm -frv ./_projects/
rm -frv ./_projects_templates/
rm -frv ./_site/
rm -frv ./assets/

Update: Jan 6, 2022

The dark theme has been updated to keep the black font color on progress bars:

.progressbar > div {
  color: black;
}

Update: Mar 9, 2023

This site (andrewburch.me) also now operates as a Tor hidden service (puy5fvdhbp3pxk6hnk6bhhkpy7l3kmtmr75aqoru5mi72rjsfvnxpbad.onion). I will not be setting up TLS on the .onion clone as doing so would be redundant; encryption to hidden services is already end-to-end and HTTPS would be counter-productive given one purpose of HTTPS is to de-anonymize the server.

Update: Sep 30, 2023

I have added gem liquid_reading_time to automatically calculate the estimated reading time for every post and project on the site. This also frees up the description field, hence why every article on the site now contains both. The site will also now be minified on build with jekyll-minifier for optimization purposes.

Update: Nov 24, 2023

MAT2 has been implemented into my build.sh script to automate removal of metadata from site assets. I also added a disclaimer to my contact page encouraging visitors to use PGP.

.disclaimer {
	border: 0.075rem solid #ff1744;
	border-radius: 0.2rem;
	margin: 1.5625em 0;
	padding: 0 0.6rem;
}

.disclaimer > p:first-child {
	background-color: #ff17441a;
	font-size: 15px;
	margin: 0 -0.6rem;
	padding-bottom: 0.4rem;
	padding-left: 1rem;
	padding-top: 0.4rem;
}

.disclaimer > p:last-child {
	font-size: 14px;
	line-height: 1.6;
	margin: 0 -0.6rem;
	padding-bottom: 0.8rem;
	padding-left: 1rem;
	padding-right: 1rem;
	padding-top: 0.8rem;
}

Aside from the disclaimer, the form will now encrypt the textarea value with my PGP public key (stored inside a hidden div) before it POSTs to Formspree. This was accomplished with OpenPGP.js (specifically, openpgp.min.js). Conveniently, Proton Mail has taken over as primary maintainer of the OpenPGP.js project. JavaScript code follows.

async function encrypt() {
  if(window.crypto.getRandomValues) {
    const textarea = document.getElementById('message');
    const plaintext = textarea.value;
    if(plaintext != '') {
      const form = document.forms[0];
      if((!(plaintext.startsWith('-----BEGIN PGP MESSAGE-----')) && (!(plaintext.endsWith('-----END PGP MESSAGE-----'))))) {
        console.log('Plaintext: ' + plaintext);
        const public_key = document.getElementById('public_key').innerHTML
          .replace('-----BEGIN PGP PUBLIC KEY BLOCK-----', '-----BEGIN PGP PUBLIC KEY BLOCK-----\n');
        console.log('Public key: ' + public_key);
        openpgp.encrypt({
          message: (await openpgp.createMessage({
            text: plaintext
          })),
          encryptionKeys: (await openpgp.readKey({
            armoredKey: public_key
          }))
        }).then(ciphertext => {
          console.log('Ciphertext: ' + ciphertext);
          textarea.value = ciphertext;
          form.submit();
          textarea.value = plaintext;
        });
      } else {
        console.log('Plaintext is already ciphertext.')
        const ciphertext = plaintext;
        console.log('Ciphertext: ' + ciphertext);
        form.submit();
      }
    } else {
      console.log('Plaintext cannot be empty.');
    }
  } else {
    console.log('Browser does not support cryptography.');
  }
}

Update: Mar 25, 2024

Just a quick update to add an automatically generated table of contents to each post and project on the site using jekyll-toc and email address “protection” with jekyll-email-protect. I also sidelined my previous disclaimer code in favor of the premonition plugin.

Update: Mar 26, 2024

Follow up to yesterday’s update - dark theme has been updated to adjust all .section-nav table of contents elements and .premonition blockquotes along with the theme. I also had to modify hook.rb to hook into my projects to get the .premonition blockquotes working on those documents. Ruby code follows.

Hooks.register [:projects], :pre_render do |doc|
  if process?(resources, doc)
    doc.content = processor.adder(doc.content)
    doc.data['excerpt'] = Jekyll::Excerpt.new(doc) if generate_excerpt? doc
  end
end

Update: May 26, 2024

My online résumé formatting and style were both in need of a touch up so I made some adjustments (I may continue adjusting further). Also added support for FontAwesome.