This article was written by Claude (Anthropic’s AI assistant) based on conversations with and direction from the blog’s human author, who made all of the technical and design decisions.
Why Build Your Own?¶
Every ML engineer I know has the same problem: we want to share our work, but the available tools are either too heavy (WordPress, Ghost) or too opinionated (Medium, Substack) for technical content with lots of code, maths, and diagrams.
So we built something minimal — a Flask app that reads Markdown files from disk, renders them with syntax highlighting, and serves them behind a clean UI. No database, no admin panel, no JavaScript framework. Just files, git, and Python.
The Architecture¶
The entire stack is deliberately boring. Content lives as Markdown files with YAML frontmatter in a content/ directory, organised by project. Projects are declared in a single content/_projects.yaml file. At startup, every .md file is parsed into an in-memory index. For a personal blog, this means sub-millisecond reads and zero moving parts at request time.
# The whole app factory
def create_app():
app = Flask(__name__)
app.content = ContentManager("content")
app.register_blueprint(routes)
return app
The ContentManager handles everything: YAML frontmatter parsing (six lines of stdlib, no external dependency), Markdown rendering via python-markdown with Pygments for syntax highlighting and an auto-generated table of contents, and building the search index.
Content Workflow¶
Writing a new article is:
- Create a Markdown file in the right project folder
- Add frontmatter (title, date, summary, tags)
- Write the content in Markdown
- Run
./deploy.sh deployto push it live
Frontmatter is minimal by design:
---
title: "Article Title"
date: 2026-05-16
summary: "Shown in listings and search results"
tags: [python, pytorch]
draft: false
---
Setting draft: true excludes an article from all listings and search without deleting the file.
Features Built¶
Search is a JSON index served at /api/search-index and filtered entirely client-side in JavaScript. No server-side query handling, no search engine to run.
RSS is an Atom feed at /feed.xml, rendered via a Jinja2 template — no library needed.
Table of contents is auto-generated per article using the toc Markdown extension and hidden below 1100px so it doesn’t clutter mobile layouts.
Syntax highlighting uses Pygments. Here’s a PyTorch training loop to demonstrate:
import torch
import torch.nn as nn
model = nn.TransformerEncoder(
nn.TransformerEncoderLayer(d_model=512, nhead=8),
num_layers=6,
)
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
for epoch in range(num_epochs):
for batch in dataloader:
loss = model(batch)
loss.backward()
optimizer.step()
optimizer.zero_grad()
Responsive layout: the sidebar collapses to a hamburger menu below 768px. No CSS framework — just custom properties and a handful of media queries.
Security¶
Security headers are set in the app factory: Content Security Policy, X-Frame-Options, X-Content-Type-Options, and Referrer-Policy. The Docker image runs as a non-root user with a read-only root filesystem, and there are no user-writable inputs anywhere in the application.
Deployment¶
The deploy.sh script covers the full lifecycle:
./deploy.sh dev # Flask dev server (port 5000, hot reload)
./deploy.sh local # Docker build + run (port 8000, production-like)
./deploy.sh deploy # Build → push to Scaleway Registry → redeploy
./deploy.sh logs # Tail container logs
./deploy.sh stop # Stop local container
The deploy command tags every image with the git short SHA and uses the Scaleway API to PATCH the container revision. Tagging with the SHA guarantees the platform sees a configuration change and actually redeploys — without it, pushing the same latest tag can silently no-op.
The service runs on Scaleway Serverless Containers, which scales to zero between requests. For a personal blog this is essentially free, and there is no infrastructure to babysit.
Jupyter Notebook Content¶
Notebooks are showcased as static Markdown — no live kernel, no nbconvert service, no additional infrastructure. The approach is to hand-export the interesting parts: code cells become fenced Python blocks, outputs become text or image embeds, and narrative cells become prose. The full .ipynb can be linked for download if a reader wants to run it.
This keeps the blog dependency-free while still letting readers follow the code and see results.
What’s Still to Come¶
- LaTeX rendering for maths-heavy posts
- Dark mode toggle
- Real identity — the placeholder author details need replacing
That’s it. Simple things should be simple.