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.
Link rewriting
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 metahome.html– landing page with page-head section and post cardsblog_index.html– full post archivecategory.html– filtered list for a single categorypost.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.