Technology · Jun 23, 2026

Static-First Publishing

The end goal is always a static site -- here is the pipeline from edit to deploy.

The three stages

The CMS follows a three-stage pipeline: edit, build, serve. Each stage is a separate command with a single job. Nothing is magical or automatic.

Edit. Run cli.py dev to start the admin UI and JSON API on port 8000. Write posts, set front matter, assign categories. Every save writes to both SQLite and a Markdown file under content/posts/. The Markdown file is what matters. The DB row is a copy.

Build. cli.py build walks the Markdown directory, reconciles every file with the database, renders all post bodies to HTML through the Python-Markdown library (extensions: extra, codehilite, toc, smarty), and writes a complete static site tree to site/. The old output directory is wiped first so stale pages do not linger.

Serve. site/ is plain HTML, CSS, and XML. Drop it on any static host. We use nginx pointed at the directory, with a few cache rules for assets and a custom 404 page. The admin API does not need to be public. It runs on a loopback port behind a firewall or VPN.

What the builder generates

For every published post the builder writes one page at the path /en/{category}/{slug}/. Category pages list all posts under that category. The all-posts page at /en/blog/ lists everything. The home page shows recent and highlighted posts.

The builder also writes feed.xml (RSS 2.0), sitemap.xml, and robots.txt. Feed items use the post description as the summary and the publish date as the pubDate. The sitemap includes every post URL, every category URL, and the blog index.

Post bodies pass through two rewrite passes during build. First, wiki-style links to /en/... paths get rewritten to point at the wiki base URL if a wiki database is configured. Second, internal blog links like /en/blog/some-post/ get resolved to the correct category path /en/{category}/some-post/ using the slug-to-category map built from the database.

Templates and theme

The public site uses Jinja2 templates under app/site_templates/. There are six templates:

  • base.html – shared shell with header, footer, and SEO meta
  • home.html – landing page with page-head section and post cards
  • blog_index.html – full post archive
  • category.html – filtered list for a single category
  • post.html – single post with prose body and related links
  • _macros.html – reusable card and URL generators

Static assets live under app/site_static/ and get copied verbatim into site/static/ on every build. The current theme uses Sofia Pro as the typeface and a blue-on-paper color scheme matching the rest of the ktown.cloud sites.

Deploy notes

The admin process runs under PM2 as blog-cms-admin, listening on 127.0.0.1 only. The static site is served by nginx with TLS terminated at the origin using a Let’s Encrypt cert. After editing, trigger a rebuild from the admin UI or run cli.py build over SSH. The site updates in place with no downtime because nginx reads the filesystem directly.