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: nkantar.com is a Django app exported as a static site.
nkantar.com/ # repo root ├── config/ # Django app settings ├── content/ # content source files: Markdown + assets ├── manage.py # 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/ # deploy.sh, 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.
nkantar.com/content/ ├── 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:
assets/
gets processed and copied to output/
. Specifically, css/
contains .scss
files that get compiled into one .css
file which is then copied over, and fonts/
, images/
, and media/
are copied verbatim.blog/
directory contains all published posts, split into years and months for easier navigation (and better namespacing).drafts/
directory contains Markdown files of unpublished posts, which are processed in development but not at build time.errors/
contains templates for 404 and 500 errors.pages/
is a directory of Markdown files corresponding to top level pages in the site nav, e.g., About and Now.robots.txt
is a half-hearted attempt to tell search engine crawlers what to do.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 urls.py
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 nkantar.blog.models import Post
from nkantar.blog.views 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 = [
distill_path(
"<int:year>/<int:month>/<str:slug>/",
PostView.as_view(),
name="blog-post",
distill_func=distill_blog_posts,
),
]
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.
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: ./manage.py runserver } # posts content/blog/** nkantar/blog/management/commands/import_posts.py { prep: ./manage.py import_posts @mods } # drafts content/drafts/** nkantar/blog/management/commands/import_drafts.py { prep: ./manage.py import_drafts @mods } # pages content/pages/** nkantar/pages/management/commands/import_pages.py { prep: ./manage.py import_pages @mods } # stylesheets content/assets/css/** nkantar/core/management/commands/compile_css.py { prep: ./manage.py compile_css } # assets content/assets/fonts/** content/assets/images/** content/assets/media/** nkantar/core/management/commands/copy_assets.py { prep: ./manage.py 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:
output/
directory, just in case.output/
.The result is a fully static site that has minimal overhead and (theoretically) no security risk. 🚀
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.