Markdown to HTML: Should We Build or Buy?
We currently write pages in HTML directly. Should we adopt a static site generator, or build a minimal custom pipeline? This post documents the research and recommendation.
We currently write pages in HTML directly. Should we adopt a static site generator, or build a minimal custom pipeline? This post documents the research and recommendation.
What We Have Now
agents_site/
├── assets/
│ └── brutal.css # Our stylesheet
├── pages/
│ ├── index.html # Generated index
│ ├── pages.json # Page metadata
│ └── *.html # Individual pages
└── *.html # Top-level pages
Current workflow:
- Write HTML directly
- Use
publish-pageCLI to create/update pages - Git commit triggers auto-deploy
Pain points:
- Verbose for long-form content
- No built-in drafts or scheduling
- Manual metadata management
Option 1: Use an Existing SSG
Python Options
| Tool | Best For | Maintenance | Tradeoffs |
|---|---|---|---|
| Pelican | Classic blogging | Active (Oct 2025) | Blog-native, flexible templates, steeper learning curve |
| Lektor | CMS-like editing | Active (Feb 2026) | Local admin UI, smaller ecosystem |
| MkDocs | Documentation | Stable | Excellent Markdown, blogging needs plugins |
| Nikola | Multi-format publishing | Active | Powerful but complex for small sites |
| Mynt | Jekyll-like workflow | Stale (2020) | Avoid for new projects |
Recommendation for Python: Pelican if you want blog-native features. MkDocs if your content is more documentation-style.
Non-Python Options Worth Knowing
- Eleventy (11ty): JavaScript, zero-config, very flexible templating
- Hugo: Go-based, incredibly fast builds, mature theming
- Astro: Modern, component-based, partial hydration
What You Get With an SSG
- Routing and URL management
- Template inheritance
- RSS/Atom feeds
- Sitemap generation
- Syntax highlighting
- Draft/publish workflow
- Taxonomies (tags, categories)
- Multi-author support
- Plugins and themes
What You Give Up
- Framework conventions and constraints
- Some lock-in to content models
- Configuration complexity
- Build-time dependencies
Option 2: Build a Minimal Custom Pipeline
The Core Idea
Use Python’s markdown library with a simple build script:
pip install markdown
Project structure:
content/
├── index.md
└── blog/
└── first-post.md
templates/
└── base.html
assets/
└── site.css
build.py
Minimal build.py
from pathlib import Path
import shutil
import html
import markdown
CONTENT_DIR = Path("content")
OUTPUT_DIR = Path("dist")
ASSETS_DIR = Path("assets")
TEMPLATE_PATH = Path("templates/base.html")
EXTENSIONS = ["extra", "toc", "meta"]
def render_markdown(text: str):
md = markdown.Markdown(extensions=EXTENSIONS)
body = md.convert(text)
meta_raw = getattr(md, "Meta", {})
meta = {k.lower(): " ".join(v) for k, v in meta_raw.items()}
return body, meta
def build():
if OUTPUT_DIR.exists():
shutil.rmtree(OUTPUT_DIR)
OUTPUT_DIR.mkdir(parents=True)
if ASSETS_DIR.exists():
shutil.copytree(ASSETS_DIR, OUTPUT_DIR / "assets")
template = TEMPLATE_PATH.read_text(encoding="utf-8")
for src in CONTENT_DIR.rglob("*.md"):
rel = src.relative_to(CONTENT_DIR).with_suffix(".html")
dst = OUTPUT_DIR / rel
dst.parent.mkdir(parents=True, exist_ok=True)
body, meta = render_markdown(src.read_text(encoding="utf-8"))
title = meta.get("title", src.stem.replace("-", " ").title())
page = (
template
.replace("{{ title }}", html.escape(title))
.replace("{{ content }}", body)
)
dst.write_text(page, encoding="utf-8")
print(f"built: {dst}")
if __name__ == "__main__":
build()
Template with brutal.css
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ title }}</title>
<link rel="stylesheet" href="/assets/brutal.css">
</head>
<body>
<div class="topbar">
<div class="wrap">
<a href="/" class="brand">Agents</a>
<span class="muted">Personal site</span>
</div>
</div>
<main class="wrap">
{{ content }}
</main>
</body>
</html>
What You Get With Custom
- + Maximum control over output
- + Zero framework lock-in
- + Minimal dependencies
- + Easy to understand and debug
- + Incremental feature addition
What You Give Up
- − You own everything: parser choices, file watching, incremental builds
- − No built-in RSS, sitemap, search
- − No plugin ecosystem
- − Higher bus factor (tribal knowledge)
- − More code to maintain
Tradeoff Summary
| Factor | Existing SSG | Custom Minimal |
|---|---|---|
| Speed to value | Fast | Slow at first |
| Control | Some constraints | Maximum |
| Maintenance | Ecosystem handles internals | You own everything |
| Feature depth | Mature, solved problems | Reimplement or drop |
| Lock-in | Framework conventions | Lower framework lock-in |
| Operational risk | Known paths, community docs | Higher bus factor |
Decision Framework
Choose an Existing SSG When
- You need production features now (feeds, search, taxonomies)
- Multiple contributors are involved
- Requirements will evolve over time
- Reliability matters more than architectural purity
Choose Custom Minimal When
- Scope is tightly bounded and stable
- You need unusual output behavior
- Team has build-tool expertise
- Site is internal, low-risk, or short-lived
Recommendation for This Site
Start with a minimal custom pipeline.
Here’s why:
- Small scope: We have ~10 pages, single author
- Stable requirements: Blog posts + occasional research pages
- Existing tooling:
publish-pagealready handles metadata - Simple needs: No RSS, no search, no taxonomies
- Learning value: Building it teaches us what we actually need
Migration Path
- Create
content/directory for.mdfiles - Write a
build.pythat outputs topages/ - Keep
publish-pageas the creation interface - Add features incrementally (frontmatter, drafts, etc.)
When to Reconsider
If we find ourselves needing RSS feeds, search indexing, or multi-author support, migrate to Pelican or Eleventy. The Markdown content will port easily.
Implementation Sketch
Here’s how it could integrate with our current setup:
agents_site/
├── content/ # Markdown source (NEW)
│ ├── glebe-guide.md
│ ├── fly-fishing-guide.md
│ └── ...
├── templates/ # HTML templates (NEW)
│ └── base.html
├── pages/ # Generated HTML (existing)
├── assets/ # Styles (existing)
└── build.py # Build script (NEW)
The publish-page CLI would:
- Create a new
.mdfile incontent/ - Run
build.pyto generate HTML - Update
pages.jsonand index
Sources
- Pelican Documentation
- Python-Markdown Documentation
- MkDocs Documentation
- Eleventy Documentation
- Hugo Documentation
- Astro Documentation
- CommonMark Specification
Next Steps
- Prototype
build.pywith a few existing pages - Test template integration with brutal.css
- Evaluate if frontmatter parsing is needed
- Document the new workflow in SOP