CrankTheCode is a small FastAPI application that serves a personal site (posts + portfolio + Decision Architecture hubs + Decision Architecture Patterns hubs + a Books catalogue page) from Markdown posts, plus a lightweight static asset fingerprinting pipeline and an EPUB book builder.
- Render starts Uvicorn against the repo-root shim:
render.yaml→main.pymain.pyexists as a compatibility layer foruvicorn main:appdeployments and localpython main.py:main.py
- Real ASGI app + app factory live in:
create_app()and module-levelapp
Both of these are defined inside create_app():
- Canonical host + https redirects (301) for non-local hosts: middleware
enforce_canonical_host_and_scheme- Canonical host constant is currently hard-coded to
www.crankthecode.com:CANONICAL_HOST
- Canonical host constant is currently hard-coded to
- Response cache policy by content-type (HTML is no-store; RSS/sitemap/robots must-revalidate): middleware
cache_policy_middleware
The codebase is intentionally simple and uses a light Ports and Adapters / Clean Architecture flavor:
- Presentation: FastAPI routers for HTML pages, JSON API, RSS, sitemap/robots.
- Application: a service façade plus use cases.
- Domain: typed dataclasses and tag parsing/normalization.
- Infrastructure/Adapters: filesystem repository, Markdown rendering strategy, static asset pipeline + caching.
Key types:
- Service façade:
BlogService - Use cases:
ListPostsUseCase.execute(),GetPostUseCase.execute() - Domain models:
MarkdownPost,PostSummary,PostDetail - Ports:
PostsRepository,MarkdownRenderer - Adapters:
FilesystemPostsRepository,PythonMarkdownRenderer
Composition root for the blog service lives in the presentation layer dependency module:
get_blog_service()wires repo + renderer + use cases.
flowchart TD
U[Browser / crawler / feed reader] --> APP[FastAPI app]
APP --> M1[Canonical redirect middleware]
APP --> M2[Cache policy middleware]
APP --> HTML[HTML router]
APP --> API[API router]
APP --> RSS[RSS router]
APP --> SEO[Sitemap/robots router]
HTML --> DI[get_blog_service]
API --> DI
RSS --> DI
SEO --> DI
DI --> SVC[BlogService]
SVC --> LUC[ListPostsUseCase]
SVC --> GUC[GetPostUseCase]
LUC --> REP[PostsRepository]
GUC --> REP
LUC --> MR[MarkdownRenderer]
GUC --> MR
REP --> FS[FilesystemPostsRepository]
MR --> REND[PythonMarkdownRenderer]
FS --> MD[posts/*.md]
HTML --> TPL[Jinja templates]
APP --> STA[Static mounts: /static, /docs]
STA --> CFS[CachingStaticFiles]
REND --> ASM[AssetManifest URL rewriting]
- Route handler:
homepage() - Shared request context (canonical URL, query state, defaults):
_base_context() - Homepage JSON-LD is created server-side and emitted via template slots:
jsonld_json
The homepage is intentionally a gateway (not a post listing).
It contains:
-
A featured single-post CTA for the thesis post:
- Source post:
posts/OODAIntro.md - Link target:
/posts/OODAIntro
- Source post:
-
Two gateway cards routing users into the two Decision Architecture ecosystems:
-
Decision Architecture (Structures) →
/decision-architecture -
Decision Architecture Patterns →
/patterns
Template: templates/index.html
Visual structure on the homepage is intentionally separated by the green “pill” divider:
- Separator component:
post-separator
- Route handler:
books_page() - Template:
templates/books.html - Book metadata is centralized to avoid duplication:
The Books page presents a calm visual catalogue (covers only, linked to Amazon) with restrained spacing.
- Route handler:
posts_index() - Default behaviour hides blog posts from listing unless explicitly included via
exclude_blog=0-like truthy values:exclude_blog - Filtering inputs:
- legacy
q=cat:<Label>is still supported viacurrent_q:current_q - newer query params
cat=<Label>andlayer=<slug>are also tracked in base context:current_cat,current_layer
- legacy
- Route handler:
read_post() - Content is produced by the application layer:
GetPostUseCase.execute() - Canonical URL preserves query string (e.g. for filtered listings):
canonical_url_for_request() - Meta description is built from frontmatter (blurb first, then one-liner fallback):
build_meta_description() - JSON-LD is emitted by the base template in two slots (primary + optional extra graph):
templates/base.html
There are two parallel “layer hub” systems.
- Gateway (grouped listing by
layer:):/decision-architecture- Route:
decision_architecture_gateway() - Template:
templates/decision_architecture.html
- Route:
- Layer hubs (per-layer listing):
/topics/<layer>- Route:
topic_hub_page() - Template:
templates/topic_hub.html
- Route:
- Gateway (grouped listing by
layer:):/patterns- Route:
patterns_index() - Template:
templates/patterns_index.html
- Route:
- Layer hubs (per-layer listing):
/patterns/<layer>- Route:
patterns_layer_page() - Template:
templates/patterns_hub.html
- Route:
/topics is the shared “View all layers” destination for both ecosystems.
It renders two pill rows (and intentionally does not duplicate those destinations as a second hub-list section):
- Patterns layers →
/patterns/<layer> - Structures layers →
/topics/<layer>
Route + template:
- RSS feed:
rss_feed()- Excludes Leadership/Decision-Architecture stream:
_is_leadership_post() - Uses Media RSS thumbnails + CDATA for HTML bodies:
_wrap_cdata()
- Excludes Leadership/Decision-Architecture stream:
- Sitemap:
sitemap_xml() - Robots:
robots_txt()
Posts are posts/*.md files with YAML frontmatter loaded via the filesystem repository: FilesystemPostsRepository.
The post storage model is: MarkdownPost. In addition to title, date and tags, it supports:
blurb(used for meta description and list UI)one_liner(used for social preview snippets)image(explicit cover image; also used as default thumbnail)thumb_image(optional tile thumbnail; falls back toimage):thumb_imageemoji(optional visual thumbnail):emojisocial_image(OpenGraph/Twitter image; falls back to cover):social_imageextra_images(gallery/screenshot images):extra_images
- Tags are normalized to a list of strings:
FilesystemPostsRepository._normalize_tags() - Dates are normalized to a sortable string format currently stored as
YYYY-MM-DD HH:MM:FilesystemPostsRepository._normalize_published_at() - Safety-net: files named
blog*.mdare always discoverable undercat:Blogeven if missing the tag:slug.lower().startswith("blog")
Rendering is intentionally a use case concern (so HTML shape stays testable and consistent):
- List views extract the first paragraph as a summary and handle cover/thumb selection/stripping:
ListPostsUseCase.execute() - Detail views strip an explicit cover image paragraph near the top, protect author screenshots sections and can inject a controlled Screenshots section from
extra_images:GetPostUseCase.execute()- Special-case AxisDB: inject an install prompt snippet after the Problem→Solution→Impact section:
_axisdb_install_prompt_markdown()
- Special-case AxisDB: inject an install prompt snippet after the Problem→Solution→Impact section:
The site uses simple tag conventions to drive grouping and navigation:
- Primary category tags:
cat:<Label>- Decision Architecture (Structures) is
cat:Leadership. - Patterns is
cat:decision-architecture-patterns.
- Decision Architecture (Structures) is
- Layer tags:
layer:<slug>- Used for grouping into layers (both ecosystems).
- Normalization/humanization is shared:
normalize_layer_slug()andhumanize_layer_slug()
Decision Architecture (Structures) hubs are derived from cat:Leadership + layer: tags:
Patterns hubs are derived from cat:decision-architecture-patterns + layer: tags:
- Jinja environment is created in the app factory and stored in app state:
fastapi_app.state.templates - Global helper
asset_url()is exposed to templates for fingerprinted URLs:env.globals["asset_url"] - Base template owns SEO and JSON-LD slots and the global sidebar:
templates/base.html
Sidebar navigation is intentionally explicit/hardcoded (not derived from tags):
- Sidebar template:
templates/base.html
The sidebar is styled + sized in CSS:
- Layout column width (sidebar vs content):
static/styles.css
Sidebar section headers are clickable (and highlight when any child route is active):
- Template:
templates/base.html - Styling:
sidebar-link--section
Static files are served via a caching-aware StaticFiles subclass:
- Static mount:
fastapi_app.mount("/static", ...) - Primary implementation:
FallbackStaticFiles+CachingStaticFiles- Fingerprinted filenames cache for 1y immutable:
CachingStaticFiles.get_response() - Non-fingerprinted assets are
no-cache, must-revalidateas a safe fallback:Cache-Control
- Fingerprinted filenames cache for 1y immutable:
The app mounts /docs for public artifacts like the CV: fastapi_app.mount("/docs", ...)
EPUB files are retained in-repo but are no longer published under /docs.
Render runs an explicit build step before starting the app: buildCommand.
- Builder script:
build_static_dist()- Copies every file to its original path in
static_dist/and emits a fingerprinted copy. - Writes
manifest.jsonmapping logical rel-path → fingerprinted rel-path.
- Copies every file to its original path in
- Manifest loader + URL rewriting:
- Load:
AssetManifest.load() - Template helper:
asset_url() - Rewrite
/static/...URLs inside rendered HTML:rewrite_html_static_urls()
- Load:
Runtime selection:
- The app serves from
static_dist/when present, otherwise falls back tostatic/:static_dir - Environment overrides:
CTC_USE_STATIC_DISTtoggles serving fromstatic_dist/when present:use_static_distCTC_STATIC_DIST_DIRselects the served static directory:configured_static_dist_dir
The repository also ships a small book-building subsystem that compiles selected posts into an EPUB.
- Script entrypoints:
- Orchestrator use case:
BuildOrchestrator.build()- Reads source posts from
posts/(filtered by tags depending on the build script):FilesystemBookPostsRepository.list_posts() - Uses shared tag logic from the app domain to keep layer parsing consistent:
primary_layer_slug_from_tags()
- Reads source posts from
- Output is written under a non-public directory so EPUBs are retained but not served by the app:
- Decision Architecture:
output_file→book/private_epubs/Decision-Architecture.epub - Patterns build mirrors this to
book/private_epubs/decision-architecture-patterns.epub:PatternsBookPaths.from_repo_root()
- Decision Architecture:
- EPUB build is executed by calling
pandocvia subprocess:- Command construction + up-to-date checks:
PandocEpubBuilder.build()
- Command construction + up-to-date checks:
The test suite doubles as architecture enforcement by locking in outward behaviour (routing, SEO, caching and deterministic HTML output).
- Canonical redirect middleware behaviour:
test_canonical_redirect_middleware_redirects_http_apex_to_https_www() - Entrypoint shim coverage (
python main.pypath without starting Uvicorn):test_root_main_module_can_run_as_script_without_starting_server()