Goodbye Jekyll, Hello Pelican.
I've been using Jekyll to generate this site on GitHub Pages for many years now. It's been a great tool, but I've been working towards simplifying my technical stack and Jekyll is unfortunately the only reason I have Ruby installed on my system.
I've finally settled on Pelican. It checks off quite a few boxes for me:
- It's written in Python, which is a default install on most systems.
- It uses Jinja2 for templating, which I already use in many, many projects.
- 11k stars on GitHub and frequent commits. We won't be stuck maintaining it.
- I can use Markdown, or I can reStructuredText, which allows me to re-use content I've written for the sphinx documentation generator.
- I can plug in my own Markdown parser, so I can fix some issues that bug me when trying to style the generated HTML.
- It's something I haven't used before, and mixing it up sometimes is good for the soul :)
Making a Theme
The old version of this blog used a custom theme I wrote for Jekyll that was fairly minimal. Still, I wasn't happy with it how it ended up looking and a cold page load of the index was a horrifying 76.8kb! Some color contrasts would also be difficult for someone with poor vision to read. So the new theme needs to have meet a couple of requirements:
- It needs to be small. The initial page load should be ~10kb.
- It needs to be accessible
- Colors are high contrast and the font is legible.
- Screen readers should have no problem navigating the site.
- It needs to look decent at all screen widths.
- It needs to be simple to maintain, with minimal design and markup.
- Should score 100 across the board on Google's Lighthouse audit.
Pelican lets us be very lazy with our theme. I don't write nearly enough to
need pagination of the article list, or have separate pages for categories, so
we simply...don't write them. We'll create a theme
directory with the bare
minimum that looks like this:
theme
├── static
│ └── css
│ └── body.css
└── templates
├── article.html
├── body.html
├── header.html
└── index.html
We create a barebones HTML5 template in body.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block title %}{{ SITENAME }}{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{ SITEURL }}/theme/css/body.css" rel="stylesheet">
<script defer data-domain="tkte.ch" src="https://plausible.io/js/script.js"></script>
</head>
<body>
{% include "header.html" %}
{% block content %}
{% endblock %}
</body>
We create a simple header in header.html
:
<header class="header">
<nav>
<h1><a href="{{ SITEURL }}/">{{ SITENAME }}</a></h1>
</nav>
</header>
We create a simple article template in article.html
:
{% extends "body.html" %}
{% block content %}
<main>
<article>
<header>
<h1>{{ article.title }}</h1>
</header>
<section>
{{ article.content }}
</section>
</article>
</main>
{% endblock %}
And finally an index template in index.html
:
{% extends "body.html" %}
{% block content %}
<main>
<article class="intro">
<section>
<p>Hi! I'm Tyler Kennedy. You can find me on
<a href="https://github.com/tktech">GitHub</a>
or by <a href="mailto:tk@tkte.ch">email</a>.
</p>
<p>Looking for my <a href="{{ SITEURL }}/data/pgp.asc">PGP key?</a></p>
</section>
</article>
<section class="articles">
{% if articles %}
<ul>
{% for article in articles_page.object_list %}
<li>
<span class="mono">[ {{ article.date.strftime('%Y-%m-%d') }} ]</span>
<a href="{{ SITEURL }}/{{ article.url }}">{{ article.title }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</section>
</main>
{% endblock %}
Finally, we tell Pelican to use our theme by adding the following to our
pelicanconf.py
:
THEME = 'theme'
And that's it, we have our custom theme. So far, it looks like I've managed to tick off every box. The initial page load is at 10.2kb, which includes a CSS reset, quite a bit of un-minified CSS, analytics, and a bunch of CSS rules for syntax highlighting with pygments.
Fixing Links
I don't want to break all of the links to my old articles, so I need to make
sure that the new site generates the same URLs. This is fairly easy to do with
Pelican, as it has a slug
setting that lets us set the URL for each article,
combined with changing the global pattern for URL generation. We do this by
adding a couple of settings to the pelicanconf.py
file:
# Change the URL pattern to match the old Jekyll one.
ARTICLE_URL = 'articles/{date:%Y}/{date:%m}/{date:%d}/{slug}.html'
ARTICLE_SAVE_AS = 'articles/{date:%Y}/{date:%m}/{date:%d}/{slug}.html'
Analytics
We don't want full-blown analytics, but it's always nice to know when someone has linked to an article, so you can participate in any conversation. I've decided to give Plausible a try. It's a minimal (<1kb) and self-host-able (but in this case, we'll use their hosted version) analytics tool that doesn't track any personal information (PI).
Deploying
The preferred method of deploying to GitHub pages seems to have changed since
I setup this blog. Instead of deploying the gh-pages
branch of your git repo,
you can use a GitHub action to build the site and deploy it. We'll start with
GitHub's template for static HTML and tweak it a bit:
name: Deploy static content to Pages
on:
push:
branches: ["master"]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Setup python
uses: actions/setup-python@v2
with:
python-version: "3.10"
- name: Install python requirements
run: pip install -r requirements.txt
- name: Build pelican site
run: pelican content -o output -s publishconf.py
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
path: 'output'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
We also need to put our build requirements into a requirements.txt
at the
root of the site:
blinker==1.6.2
docutils==0.20.1
feedgenerator==2.1.0
Jinja2==3.1.2
Markdown==3.4.4
markdown-it-py==3.0.0
MarkupSafe==2.1.3
mdurl==0.1.2
pelican==4.8.0
Pygments==2.16.1
python-dateutil==2.8.2
pytz==2023.3
rich==13.5.2
six==1.16.0
Unidecode==1.3.6
With this, everytime we push to the master
branch, GitHub will build the
site, and deploy it to GitHub Pages. If you're reading this, it worked!
TODO
I like to emphasize <code>
, <aside>
, and <blockquote>
elements with a
background color that stretches across the entire width of the page. For our
syntax highlighted code blocks and asides this is easy, but for blockquotes
it's a bit more difficult. By default, the Markdown generator Pelican uses
doesn't emit any elements around a blockquote, or any elements within it, so we
have no way to style it so that the background stretches the entire page while
the content is limited to the column width of 768px. We need to implement
our own version of the MarkdownReader class that emits every block element
with a <div>
wrapper, and then we can style the blockquote as we wish.