Table of Contents
Overview
Site content is managed as Markdown files stored in a Git repository. The theme is a modified version of Bulma. The site is statically generated by Hugo.
I create and edit site content with Vim and preview the results
locally with hugo serve -D ...
. Articles and blog posts are saved as
drafts and committed to the Git repository so I can work on them as
time permits.
History
From 1998 to 2019 this site was generated by a custom PHP backend, written by me. In 2019 I resurrected the site after a several year hiatus and migrated the site from the custom PHP backend to Jekyll. In 2021 I switched from Jekyll to Hugo.
Goals
The content and layout of this site should be:
- Small
- Fast
- Secure
- Accessible
- Mobile-friendly
These goals are addressed as follows:
- Small: Hugo is configured to aggressively pack content and minify assets. Images are compressed and served in multiple formats. HTTP compression is enabled (with caution). See Hugo Configuration, Images and Apache Configuration.
- Fast: Static content can be served quickly. Assets are fingerprinted and browser caching is enabled. See Hugo Configuration and Apache Configuration.
- Secure: Static content; no web-accessible endpoint which can make changes. Additional security measures are discussed in Deployment, Apache Configuration, and Other.
- Accessible: Addressed when creating content and with custom shortcodes. See HTML and Hugo Configuration.
- Mobile-friendly: Addressed when creating content and with the site theme. See HTML and Bulma Configuration.
Content
This section describes how content is created for this site.
HTML
The site content and layout is designed to be accessible and mobile-friendly:
- links and table components have
title
andaria-label
attributes - images have captions, fallback formats, and
title
andalt
attributes - images scale gracefully to thumbnails on mobile
- the site supports dark mode, checks the color scheme preference to determine the default theme, and has a manual theme switcher
- The menu bar collapes to a hamburger menu on mobile.
Here are a few articles which cover guidelines that I follow:
- Progressive Enhancement
- Why your website should be under 14kB in size
- 5 things you don’t need JavaScript for
Images
Images are created as follows:
- Mathematical content is created using Mathy, saved as SVG, minified using minify, and served cached and compressed by Apache.
- Charts are created with Matplotlib, saved as SVG, minified with minify, and served cached and compressed by Apache Example: This Python script was used to generate the bar charts in this blog post.
- Lossless raster images (e.g. screenshots) are scaled and cropped with GraphicsMagick and served as a lossless WebP with a PNG fallback compressed by pngquant.
- Lossy raster images (e.g. pictures) are scaled and cropped with GraphicsMagick and served as a lossy WebP with a JPEG fallback.
Other notes:
- The animated site logo is an SVG generated by this Ruby script.
- Menubar icons are borrowed from Bootstrap Icons.
- I reviewed several PNG compressors in this post.
Managing images manually helps to keep the site small and fast. Here are a couple common GraphicsMagick commands:
# scale with antialiasing and convert from png to webp
gm convert -antialias -scale 1024x1024 some-image.{png,webp}
# get image dimensions
gm identify some-image.png
# convert from png to webp (lossless)
gm convert -quality 100 -define webp:lossless=true some-image.{png,webp}
# crop image to 256x256
gm convert -crop 256x256 some-image{,-cropped}.png
JavaScript
This site has almost no JavaScript, by design. The JavaScript that is used is minimal, modern, deferred, and only used for the theme switcher and burger menu.
Below is the unminified script.js
for this site with some notes
removed. It is served as 777 bytes minified and 586 bytes minified and
compressed:
'use strict';
//
// script.js - script which handles:
//
// - set theme
// - theme switcher and burger menu event handlers
//
const D = document,
C = D.body.parentElement.classList,
L = localStorage,
M = window.matchMedia,
on = (el, id, fn) => el.addEventListener(id, fn);
// use theme if set, otherwise fall back to browser preference
if (L && L.theme && L.theme === 'dark') {
C.add('dark'); // theme set to "dark"
} else if ((!L || !L.theme) && M && M('(prefers-color-scheme: dark)').matches) {
C.add('dark'); // prefers dark color scheme
}
document.addEventListener('DOMContentLoaded', () => {
// theme toggle event handler
on(D.querySelector('.navbar-item[data-id="theme"]'), 'click', (e) => {
e.preventDefault(); // stop event
L.theme = C.toggle('dark') ? 'dark' : 'light'; // toggle
});
// iterate through burgers, bind to click events
D.querySelectorAll('.navbar-burger').forEach(e => on(e, 'click', () => {
// then toggle is-active on burger and menu
[e, D.getElementById(e.dataset.target)].forEach(
e => e.classList.toggle('is-active')
)
}));
});
Deployment
Once I am ready to apply any outstanding changes to the public web site,
I push the outstanding commits to a remote Git repository. This
triggers a post-receive
Git hook, which sends an
HMAC-authenticated POST
request to a web hook endpoint on the
web server.
The web hook verifies the HMAC and then runs a deployment script which does the following:
- Verifies the authenticated timestamp (to prevent replay attacks).
- Clones the upstream repository.
- Executes Hugo (
hugo --minify -d ...
) to build the site in an isolated output directory. - Updates the
htdocs
symlink for the public-facing web site to point at the output directory from the previous step. - Removes any stale builds.
Configuration
This section discusses the configuration for Apache, Bulma, Hugo, and Webhook.
Apache Configuration
This section disusses the Apache configuration for this site. The information is divided into several sub-sections in order to make it easier to digest.
This site relies on the following Apache modules:
- mod_deflate: Enable HTTP compression.
- mod_http2: Enable HTTP/2.
- mod_macro: Simplify common configuration.
- mod_proxy: Proxy web hook requests to internal webhook daemon.
- mod_rewrite: Unconditionally redirect from HTTP to
HTTPS, strip
www.
from the path hostname, and redirect from legacy URLs.
Virtual Host Configuration
The Apache virtual host configuration is modified as follows:
- Unconditionally redirect from HTTP to HTTPS
- Unconditionally redirect to strip the
www.
hostname prefix - Enable HTTP/2
- Set security headers (discussed in Security Headers)
- Enable aggressive caching of image, script, and style assets
Below is the Apache virtual host configuration for this site with extraneous comments and configuration for logging, TLS, and legacy redirects removed:
# unconditionally redirect to https://pablotron.org
<VirtualHost *:80>
RewriteEngine On
RewriteRule ^/(.*)$ https://pablotron.org/$1 [R,L]
</VirtualHost>
<VirtualHost *:443>
# strip "www." prefix and enable mod_deflate
Use STRIP_WWW https://pablotron.org
Use MOD_DEFLATE
# enable http2
Protocols h2 http/1.1
# set restrictive content security policy
Header append "Content-Security-Policy" "default-src 'self'; img-src 'self' https://pmdn.org"
# set remaining security headers
Header append "Strict-Transport-Security" "max-age=31536000"
Header append "X-Frame-Options" "SAMEORIGIN"
Header append "X-Content-Type-Options" "nosniff"
Header append "Cross-Origin-Opener-Policy" "same-origin"
Header append "Cross-Origin-Resource-Policy" "same-origin"
Header append "Access-Control-Allow-Origin" "https://pablotron.org"
Header append "Referrer-Policy" "strict-origin-when-cross-origin"
# set permissions policy
Header append "Permissions-Policy" "camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), usb=()"
# POST needed for /hooks
Header append "Access-Control-Allow-Methods" "POST, GET, HEAD, OPTIONS"
# cache images, stylesheets, and javascript for 1 year
<FilesMatch "\.(ico|jpg|jpeg|png|gif|webp|svg|js|json|css)$">
Header set Cache-Control "max-age=31536000, public"
</FilesMatch>
# allow style-src-attr unsafe-inline for svgs
# (without this svgs do not render in firefox)
<FilesMatch "\.svg$">
Header set "Content-Security-Policy" "default-src 'self'; img-src 'self'; style-src-attr 'self' 'unsafe-inline'"
</FilesMatch>
# expose webhook
<Location /hooks/>
ProxyPass "http://localhost:9000/"
ProxyPassReverse "http://localhost:9000/"
</Location>
</VirtualHost>
HTTP Compression
HTTP compression is supported via mod_deflate.
It is safe for this site to enable mod_deflate because it does not use cookies and is not vulnerable to BREACH.
In 2022 I tried mod_brotli, but the improvement over mod_deflate was minimal (deflate: 125k, brotli: 117k) so I abandoned it.
Security Headers
This site uses a strict Content-Security-Policy header; it rejects links to all external assets with one exception: references to images hosted on https://pmdn.org/ are allowed.
The trickiest part of restricting Content-Security-Policy was
style-src
; many content generation tools (including the Markdown
table generator in Hugo) break without style-src 'unsafe-inline'
(e.g., the ability to emit arbitrary style
attributes).
In order to work around the style-src
issues I ended up reconfiguring
the Hugo syntax highlighter and writing a custom
Hugo table shortcode which only emits class
attributes for inline styling.
The journey to a strict Content-Security-Policy is documented in this series of blog posts from October 2021:
- Hugo/Content-Security-Policy Impedance Mismatch (October 19, 2021)
- TLS and Header Fixes (October 21, 2021)
- The Nuclear Option (No More unsafe-inline) (October 25, 2021)
The remaining security headers are explained in the following articles:
- Security headers quick reference
- Goodby Feature Policy and hello Permissions Policy!
- Permissions Policy Explainer
- Referrer-Policy
The security headers on this site earn an A+ rating from securityheaders.com:
TLS Configuration
The certificate for this site is issued by Let’s Encrypt and managed automatically by certbot with the certbot-dns-linode plugin.
The Apache TLS configuration is based on the intermediate settings from the Mozilla SSL Configuration Generator. In particular, only TLS 1.2 and TLS 1.3 are enabled, and the insecure ciphers from TLS 1.2 have been disabled.
# explicit list of cipher suites (from ssl-config.mozilla.org)
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
# use server priorities for cipher algorithm choice
SSLHonorCipherOrder on
# protocols to enable (only TLS 1.2 and TLS 1.3)
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
This TLS configuration earns an A+ result from the SSL Labs SSL Test:
Bulma Configuration
The site CSS based on the Bulma with the following modifications:
- All unused components removed
- monokai style for Chroma added
- Styles for navbar icon highlighting and table captions added
- dark mode styles added
Here is the SASS:
// style.sass: based on bulma-0.9.3/sass/bulma.sass with the following
// changes:
//
// 1. all unused components removed
// 2. monokai style for chroma added
// 3. styles for navbar icon highlighting and table captions added
// 4. dark mode styles added
@charset "utf-8"
// import chroma style
//
// generated with the following command:
//
// cd themes/hugo-pt2021/assets
// hugo gen chromaclasses --style=monokai > chroma.css
//
@import "chroma"
@import "bulma-0.9.3/sass/utilities/_all"
@import "bulma-0.9.3/sass/base/_all"
// elements
@import "bulma-0.9.3/sass/elements/button"
@import "bulma-0.9.3/sass/elements/container"
@import "bulma-0.9.3/sass/elements/content"
@import "bulma-0.9.3/sass/elements/image"
@import "bulma-0.9.3/sass/elements/table"
@import "bulma-0.9.3/sass/elements/title"
@import "bulma-0.9.3/sass/elements/other"
// components
@import "bulma-0.9.3/sass/components/media"
@import "bulma-0.9.3/sass/components/navbar"
// grid (reenabled, used for images)
@import "bulma-0.9.3/sass/grid/_all"
// helpers
@import "bulma-0.9.3/sass/helpers/_all"
// layout
@import "bulma-0.9.3/sass/layout/section"
@import "bulma-0.9.3/sass/layout/footer"
// dim navbar icons by default
.navbar-item .menu-icon
opacity: 60%
// highlight icons on hover
.navbar-item:hover .menu-icon
opacity: 100%
// table captions below tables
table.table
caption-side: bottom
// dark mode (2024-05-27)
@import "dark"
With these changes the generated CSS is 137 kB minified and 16 kB minified and compressed.
Hugo Configuration
Hugo is configured to use Chroma syntax higlighting with inline styles disabled in order to support the strict Content-Security-Policy. See Security Headers for details.
Tables are are generated by a custom table shortcode, because the Hugo’s native Markdown table generator in Hugo uses inline styles.
I have also written a couple of custom shortcodes to
generate <picture>
and <figure>
elements in order to support
progressive enhancement.
Custom archetypes have been added for the Archives section, blog posts, articles, and projects.
The generated HTML has been modified to:
- Use a custom theme. See Bulma Configuration.
- Add support for
<meta name='go-import' ...>
. - Add a Mastodon
<link rel='me' ...>
tag. - Remove all unnecessary tags.
- Combine, minify, and enable caching of JavaScript and CSS assets.
- add
integrity
attributes to<link>
and<script>
tags.
Webhook Configuration
webhook configuration with HMAC key removed:
[{
"id": "deploy-pablotron-org",
"execute-command": "/data/www/pablotron.org/git/bin/hook/deploy.rb",
"pass-arguments-to-command": [{
"source": "payload",
"name": "time"
}],
"pass-environment-to-command": [{
"source": "string",
"envname": "DEPLOY_HTDOCS_PATH",
"name": "/data/www/pablotron.org/builds/current"
}, {
"source": "string",
"envname": "DEPLOY_REPO_DIR",
"name": "/data/www/pablotron.org/git"
}, {
"source": "string",
"envname": "DEPLOY_BUILDS_DIR",
"name": "/data/www/pablotron.org/builds"
}],
"trigger-rule": {
"match": {
"type": "payload-hmac-sha256",
"secret": "(omitted)",
"parameter": {
"source": "header",
"name": "X-Hub-Signature"
}
}
}
}]
Validation
I periodically use the following tools to verify this site:
- Developer Console: Check page load time, cached and uncached page size.
- Lighthouse: Check accessibility, desktop score, and mobile score.
- Security Headers: Check HTTP security headers.
- SSL Labs SSL Test: Check TLS configuration.
I also manually check the site in the desktop and mobile versions of Chrome and Firefox.
I am investigated doing automated validation with htmltest, htmltidy, and the W3C validator, but have not added them yet.
Other
I do not store credentials (e.g., the HMAC key for the deployment web hook) in the Git repository for this site.
Write access to the Git repository for this site is only accessible via SSH.
SSH access firewalled off and is only available via Wireguard.
Here are recommendations regarding SSH, in order of preference:
- Only allow SSH access via a VPN and do not expose it to the public.
- Only allow key-based authentication and disable password authentication. Use an Ed25519 key, if possible.
- Limit the IP addresses which can connect to SSH at the firewall.
- Always disable remote
root
logins.
Note that these options are not mutually exclusive.
I have written about firewall configuration and Wireguard elsewhere on this site:
Updates
This section contains a list of updates to this article it was initially published.
- 2024-06-03: Apache Configuration: Make
SVGs render properly in Firefox by adding a
<FileMatch>
directive for SVGs which relaxes Content-Security-Policy by allowingstyle-src-attr 'unsafe-inline'
. Without this exception inline SVGs work fine, but viewing them directly just shows a black box.