Blog Migrated to Python
Once every few years this blog is ported to a new platform. Each migration attempts to simplify writing while preserving the existing content.
Here is a brief history of my blogging platforms:
- LiveJournal - that was my first blogging platform;
- WordPress - I discovered Linux and wanted to play with WordPress, like other cool kids;
- Squarespace - I didn't want to maintain the WordPress installation anymore;
- Octopress - I wanted something less expensive and more flexible than Squarespace;
- Hugo - I discovered go and wanted to have something faster and less fragile than Ruby site generator;
- Static site generator using React components - I fell in love with Node.js and React, wanted to have flexible UI components in my blog.
It isn't that hard to notice the emerging pattern here: each time a new tool shows up, promising to solve some problems. The platform ultimately gets migrated to new pastures with a greener grass. All the content, including legacy urls and asset locations, is dragged along.
A few years ago, I started spending more time with Python, getting to love its opinionated simplicity and poetic elegance. You can guess what happened next.
Current Implementation
The current iteration (source code) uses Python3 with Flask to render all the pages.
Flask is an extremely lightweight HTTP server in Python that also implements a few building blocks like url routing, request handling and template rendering. It isn't as heavy and opinionated as Django or Ruby on Rails, but is perfect for a quick API or a simple web site.
The structure of the website is determined by the content: blog posts and post series, all spread between the different folders. When the application starts, we enumerate and load all the available content files. Flask application has a generic url handler that renders the appropriate template based on content type:
@app.route('/<path:path>')
def url(path: str):
value = s.urls.get('/' + path, None)
if not value:
return "Not found", 404
if isinstance(value, blog.Story):
return render_template("story_cover_page.html", site=s, story=value)
if isinstance(value, blog.Post):
return render_template("post_page.html", site=s, post=value)
if isinstance(value, str):
return render_template("redirect.html", url=value)
HTML layout is done with Jinja templates. I grew to like them over the past years, just like Python itself.
Here is, for example, a template for the story page. It lists all posts that make up a story.
{% extends "layout.html" %}
{% from "macros.html" import link %}
{% block title %}{{ story.title }}{% endblock %}
{% block content %}
<section>
<p>{{ story.description }}</p>
{% for year, group in story._items|groupby('date.year')|reverse %}
<div>
<h3>{{ year }}</h3>
<ul>
{% for post in group|reverse %}
<li>{{ link(post, date="") }}</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</section>
{% endblock %}
Jinja markup is more constrained than React components: you can't mix HTML markup and code with the same ease. This made some features slightly more difficult to implement (e.g. "article tags" and "recommended articles"). So I just threw that functionality away. Maintaining tag lists added mental overhead anyway.
The website is static in nature. It could be rendered once and pushed to any cloud provider. This eliminates almost all of the maintenance pains and effort. There are no servers to maintain and keep updated.
Static html generation is performed by spinning up a test Flask client, requesting all of the existing urls and saving them into files. Resulting folder will then be synced to S3 bucket that is served via CloudFlare CDN.
cli = app.test_client()
urls = list(s.urls.keys())
urls.extend(['/', '/archive/', '/atom.xml', '/about-me/', '/404.html'])
for u in urls:
save_url(cli, u)
for k, v in assets.items():
shutil.copy2(v, 'build/images/' + k)
The website differs from the previous iteration by dropping a bunch of features:
- Article tags and recommendations.
- Syntax highlighting on the snippets.
- Complex and fine-tuned CSS/HTML layout.
- React.js templates (with complex inheritance).
- Re-implementation of the literate programming model that allowed to inject snippets from the code files.
- Async content processing pipelines (to speed up things).
- Drafts - articles that are rendered but don't show up in the index or home page.
Instead of that, all that we have is a few functions that load content files into a big in-memory dictionary, plus a Flask site that renders that content with Jinja templates.
Writing Flow
The writing flow itself is evolving as well. I used to write blog posts by editing markdown files in Emacs or Vim, while using the website to preview the results. This introduced unnecessary ceremony and added friction. Git folder was always dirty. Fewer posts got published.
I'm currently trying a simpler approach that is just easier: write the blog posts via a dedicated markdown writer (e.g. 1Writer on an iPad). The polished article could be then added to the site codebase and published.
Published: January 07, 2020.
🤗 Check out my newsletter! It is about building products with ChatGPT and LLMs: latest news, technical insights and my journey. Check out it out