FROM JEKYLL TO PELICAN
Building and maintaining a personal blog should be simple, but over time, technical debt can make even basic updates feel overwhelming. In 2013, I launched this blog using Jekyll as a static site generator using a mix of Ruby and NPM pagkages along with custom build scripts. While it worked well initially, maintaining the complex publishing workflow became challenging, especially after long periods without updates. Dependency issues and the need to update outdated packages turned simple post publishing into a multi-day ordeal.
To address these challenges, I knew I needed to make some changes. I established three key requirements for a new blog architecture:
- Use a single language and ecosystem to streamline management and publishing
- Implement an adaptable CSS framework
- Develop an efficient publishing workflow
After evaluating options like Pelican, Hugo, and Jekyll, I chose Pelican for its Python foundation, aligning with my recent development focus.
Pelican offered fewer ready-to-use design templates (themes) compared to other options. While some themes existed, none matched my needs. While I found a few acceptable options, building a custom theme offered the best path forward. This presented an opportunity to improve upon the hand-crafted HTML and CSS of my old site, which needed modernization to improve maintainability due to non-semantic markup and a disorganized stylesheet.
For the new theme, I opted for semantic HTML and selected Pico.css as the CSS framework. Pico's simplicity and minimal class usage made it an ideal choice for customization without adding unnecessary complexity. Customizing Pico was simple and required minimal changes and custom classes to get the look and feel I wanted.
The world of hosting options has changed dramatically since 2013. I decided to move the hosting from AWS S3 to Cloudflare Pages. When new content is pushed to Github triggers a workflow that builds the site and pushes the site to a branch that is watched by Cloudflare Pages. When the branch is updated, Cloudflare deploys changes to the live site. This is a reliable workflow that I can easily understand and maintain.
This new architecture delivered several improvements: - Simplified maintenance with a single language ecosystem (Python) - Faster content publishing through automated workflows - Improved performance with built-in minification and optimization - Better scalability using modern cloud infrastructure - Reduced costs by leveraging Cloudflare's free tier
The codebase lives on Github: natelandau/natelandau.com.
Learnings
Managing Dependencies with UV
I use uv to manage the Python environment and install the dependencies. Running uv pip compile pyproject.toml -o requirements.txt
generates a requirements.txt
file that Cloudflare Pages can use to install the necessary packages.
Essential Python Packages and Pelican Extensions
While I kept the dependency list lean, each chosen package serves a specific purpose in improving the blog:
- PyMdown: Enhances content formatting with features like syntax highlighting and smart symbols, making technical posts more readable
- image-process: Optimizes images for web delivery, improving page load times without manual intervention
- Yaml Metadata: Simplifies post management with clean, readable front matter that's easier to maintain than traditional formats
- Pelican Sitemap: Creates a
sitemap.xml
file for SEO - Pelican Neighbors: Adds a "next post" and "previous post" link to each post
- Pelican Tag Cloud: Adds a browsable tag cloud
Writing Posts with Extended Markdown
To make technical writing more expressive and readable, I leveraged PyMdown's extended Markdown features. These extensions make it easier to create rich, technical content using straightforward Markdown syntax:
==Word==
becomes Word++ctrl+alt+delete++
becomes Ctrl+Alt+Del~~Word~~
becomesWord- Inline code can be highlighted with
code
as in:import pymdownx; pymdownx.__version__
orvar test = 0;
- Emoji can be added with
:emoji_name:
as in: I have a 😄 here. and here is a 🎉 - Smart symbols are enabled by default
These extensions are configured in the pelicanconf.py
file as follows:
from pymdownx import emoji
MARKDOWN = {
"extensions": [
"pymdownx.mark",
"pymdownx.smartsymbols",
"pymdownx.tilde",
"pymdownx.saneheaders",
"pymdownx.keys",
"pymdownx.inlinehilite",
"pymdownx.emoji",
"pymdownx.extra",
],
"extension_configs": {"pymdownx.emoji": {"emoji_generator": emoji.to_png_sprite}},
}
Custom Deployment Tasks with Invoke
Invoke is a flexible Python task manager bundled with Pelican. It helps automate common development tasks, making the publishing process more efficient. Here are the key tasks I implemented:
- Post Creation: Automate the creation of new blog posts with proper formatting
- Asset Optimization: Minify HTML and CSS files
- Cache Management: Implement cache busting for better performance
- Local Development: Runs Pelican in watch mode and serve the site locally
Here's the implementation:
from datetime import datetime
import pytz
import minify_html
from pathlib import Path
from rcssmin import cssmin
POST_TEMPLATE = """\
---
title: {title}
slug: {slug}
date: {timestamp}
modified: {timestamp}
summary:
tags:
-
---
"""
def slugify(s):
s = s.lower().strip()
s = re.sub(r"[^\w\s-]", "", s)
s = re.sub(r"[\s_-]+", "-", s)
s = re.sub(r"^-+|-+$", "", s)
return s
def minify():
"""Minify all HTML and CSS files after Pelican has built the site."""
site_dir = Path(CONFIG["deploy_path"]).resolve()
for file in site_dir.glob("**/*.html"):
with open(file, "r") as f:
content = f.read()
minified = minify_html.minify(
content,
do_not_minify_doctype=True,
keep_closing_tags=True,
keep_html_and_head_opening_tags=True,
minify_css=True,
minify_js=True,
preserve_brace_template_syntax=True,
remove_processing_instructions=True,
)
with open(file, "w") as f:
f.write(minified)
print("Minified all HTML files")
for file in site_dir.glob("**/*.css"):
with open(file, "r") as f:
content = f.read()
minified = cssmin(content)
with open(file, "w") as f:
f.write(minified)
print("Minified all CSS files")
def cache_bust():
"""Cache bust links to CSS files within the HEAD by appending a unique ID to the URL."""
site_dir = Path(CONFIG["deploy_path"]).resolve()
unique_id = str(uuid.uuid4())[:8]
i = 0
for file in site_dir.glob("**/*.html"):
with open(file, "r") as f:
content = f.read()
if re.search(r'<link href="?/static/css/[a-zA-Z0-9\.-_]+\.css', content):
i += 1
content = re.sub(
r'(<link href="?/static/css/[a-zA-Z0-9\.-_]+\.css)',
rf"\1?v={unique_id}",
content,
)
with open(file, "w") as f:
f.write(content)
print(f"Cache busted CSS files in {i} files")
@task
def new(c, title):
"""Create a new post from a template.
Args:
title (str): The title of the post.
"""
newYorkTz = pytz.timezone("America/New_York")
now = datetime.now(newYorkTz)
post_dir = Path(CONFIG["post_path"]).resolve()
new_post_path = post_dir.joinpath(f"{now.strftime('%Y-%m-%d')}-{slugify(title)}.md")
new_post_path.touch()
with open(new_post_path, "w") as f:
f.write(
POST_TEMPLATE.format(
title=title,
slug=slugify(title),
timestamp=now.strftime("%Y-%m-%d %H:%M"),
)
)
print(f"Created new post at {new_post_path}")
Managing Redirects with Cloudflare Pages
Cloudflare Pages supports redirects through a _redirects
file at the root of the site. I manage redirects in the pelicanconf.py
file using the REDIRECTS
dictionary:
REDIRECTS = {
"source_url": "destination_url"
}
pelicanconf.py
file and generates the _redirects
file: {%- for src, dst in REDIRECTS.items() %}
{{ src }} {{dst}} 301
{% endfor -%}
Deployment to Cloudflare Pages
Deploying the site to Cloudflare Pages is straightforward. Any changes pushed to the main
branch trigger an automatic deployment. However, since Cloudflare Pages doesn't support uv
, I had to use pip
to install the dependencies. Luckily, uv
simplifies this process by generating a requirements.txt
file with the command uv pip compile pyproject.toml -o requirements.txt
.
Conclusion
Rebuilding my blog with Pelican, Pico.css, and Cloudflare Pages has improved the maintainability, performance, and ease of use compared to my previous Jekyll-based setup. By leveraging a single language and ecosystem, a lightweight CSS framework, and a simple deployment workflow, I've created a blog that is easier to manage and update.
The complete source code is available on GitHub for those interested in implementing similar solutions.