Table of Contents
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.