Nik Kantar

Tuesday, July 25, 2023
5 min read

Colophon: July 2023 Edition

I’ve got a reasonably ergonomic stack for this site at the moment, and I’d like to tell you about it.

Some day I might write up the history of this site, but this post is about how it all works today, as I’m currently quite happy with the ergonomics of the setup.

TL;DR: is a Django app exported as a static site.

Repo Layout        # repo root
├── config/         # Django app settings
├── content/        # content source files: Markdown + assets
├──       # Django management command runner
├── modd.conf       # daemon manager used in dev
├── nkantar/        # Django project code
├── output/         # exported static site
├── poetry.lock     # Poetry dependency lockfile
├── pyproject.toml  # Project config, catering to Poetry
└── scripts/        #, runs at build time


content/ is the most important piece of the puzzle: it’s where Markdown for all posts and pages lives, alongside all static assets used throughout the site.
├── assets/
│   ├── css/
│   ├── fonts/
│   ├── images/
│   └── media/
├── blog/
│   ├── 2014/
│   │   ├── 08/
│   │   └── ...
│   ├── ...
│   └── 2023/
├── drafts/
├── errors/
├── pages/
└── robots.txt

Much of the structure is probably self-explanatory, but here’s a quick overview anyway:

The Markdown files and static assets have stuck around through a number of different implementations, a testament to the value of using (relatively) plain text for words and self managed file structure for all the accompanying stuff.


The Django app really isn’t all that interesting—it’s mostly just a simple blog with Post and Page models, and the simple views and templates you’d expect. There are two things specific to my setup that are worth noting.


One are the management commands I added that import files from content/ into the database, which have a bit of logic to handle renaming, deletion, etc. They allow me to edit the files on disk and have the changes reflected in the web app without having to use the admin panel, touch SQL, or do anything of the sort. This makes it impossible for me to neglect any changes in version control, since the source files show up as modified.


The other is the django-distill plugin, which allows me to easily export the site as a collection of static HTML and assets. It works by wrapping URL patterns in files with a function that allows specifying a collection of path arguments that define what should be exported. The plugin then iterates through those arguments, grabs the rendered HTML, and saves it in appropriate files. This is a bit wordy, so here’s an abbreviated example:

from django_distill import distill_path
from import Post
from import PostView

def distill_blog_posts():
    posts = [
            "year": post.year,
            "month": post.month_padded,
            "slug": post.slug,
        for post in Post.objects.all()
    return posts

urlpatterns = [

The distill_path function passes everything through during development, so I get to browse the live Django site as I work on it, with the full power of Django available to me.

Writing and Development

The workflows for writing and development overlap a fair bit. As I already mentioned, I run the site locally like any other Django app, and the import scripts take care of content synchronization. But…how? Enter modd, a fantastically useful tool that allows me to run commands and start/stop daemons on file changes based on patterns. So, for a change to a Markdown file I trigger an appropriate import script, for a change to a .scss file I trigger Sass compilation and copying to the correct static/ location, for a change to a .py file I restart the Django server, etc. modd.conf probably illustrates that better than my explanation, so here’s the entire thing:

# Django dev server
**/*.py !**/commands/** {
  daemon: ./ runserver

# posts
content/blog/** nkantar/blog/management/commands/ {
  prep: ./ import_posts @mods

# drafts
content/drafts/** nkantar/blog/management/commands/ {
  prep: ./ import_drafts @mods

# pages
content/pages/** nkantar/pages/management/commands/ {
  prep: ./ import_pages @mods

# stylesheets
content/assets/css/** nkantar/core/management/commands/ {
  prep: ./ compile_css

# assets
content/assets/fonts/** content/assets/images/** content/assets/media/** nkantar/core/management/commands/ {
  prep: ./ copy_assets

Since I use Poetry to manage dependencies, I run poetry run modd and everything is up and running for me to make whatever changes I need.


This whole thing is currently deployed as a static site on Render. The deployment script goes through the following steps:

  1. Wipe the output/ directory, just in case.
  2. Wipe the local database, also just in case.
  3. Install Poetry and other dependencies.
  4. Run migrations.
  5. Import posts and pages.
  6. Compile Sass and copy all assets.
  7. Export all posts and pages as static HTML files into output/.

The result is a fully static site that has minimal overhead and (theoretically) no security risk. 🚀

drawing of laptop labeled “your device” and desktop case labeled “server” with arrow pointing from former to latter labeled “request” and another pointing from latter to former labeled “response”, and the whole thing labeled “v. fast process”

Tags: meta

Thanks for reading! You can keep up with my writing via the feed or newsletter, or you can get in touch via email or Mastodon.

See You at North Bay Python 2023
North Bay Python 2023: The Afterfeels