1 The Problem
We want a static-site generator: take a folder of Markdown blog posts and produce a folder of HTML pages plus an index linking to them all. It combines parsing, file I/O, and templating into one real tool — the kind that powers countless blogs and documentation sites.
2 How to Think About It
Think about the build pipeline, before any code:
3 The Build — explained part by part
Here is the complete generator. It reuses the heading conversion idea from the Markdown-to-HTML project. Each part is explained below.
from pathlib import Path
def md_to_html(text):
# Minimal Markdown: first # line is the title, rest are paragraphs.
html = []
title = "Untitled"
for line in text.splitlines():
if line.startswith("# "):
title = line[2:]
html.append(f"<h1>{title}</h1>")
elif line.strip():
html.append(f"<p>{line}</p>")
return title, "\n".join(html)
def page(title, body):
return f"<!DOCTYPE html><html><head><title>{title}</title></head><body>{body}</body></html>"
def build(posts_dir="posts", out_dir="site"):
posts = Path(posts_dir)
out = Path(out_dir)
out.mkdir(exist_ok=True)
index_links = []
for md_file in posts.glob("*.md"):
title, body = md_to_html(md_file.read_text())
html_name = md_file.stem + ".html"
(out / html_name).write_text(page(title, body))
index_links.append(f'<li><a href="{html_name}">{title}</a></li>')
index_body = "<h1>Blog</h1><ul>" + "".join(index_links) + "</ul>"
(out / "index.html").write_text(page("Blog", index_body))
if __name__ == "__main__":
build()
print("Site built in ./site")
# line becomes the title and an <h1>; other non-blank lines become paragraphs. It returns both the title (for the index) and the HTML.page(title, body) — a template: wrap the body in a full HTML document. Every post and the index share this template.
posts.glob("*.md") — find every
.md file in the posts folder. md_file.stem is the filename without its extension, which we reuse for the .html name.(out / html_name).write_text(...) — write the finished HTML page into the output folder.
index_links — as we go, collect a link for each post; afterwards, join them into an index page. This is the “build a table of contents” step every site generator does.
out.mkdir(exist_ok=True) before writing.a.md and a.markdown both make a.html.4 Test & Prove Each Part
We test the conversion and templating — the pure logic — without writing real files.
def md_to_html(text):
html = []
title = "Untitled"
for line in text.splitlines():
if line.startswith("# "):
title = line[2:]
html.append(f"<h1>{title}</h1>")
elif line.strip():
html.append(f"<p>{line}</p>")
return title, "\n".join(html)
def page(title, body):
return f"<!DOCTYPE html><html><head><title>{title}</title></head><body>{body}</body></html>"
def test_title_extracted():
"""The # line becomes the title."""
title, _ = md_to_html("# My Post\nHello")
assert title == "My Post"
def test_paragraph():
"""A body line becomes a paragraph."""
_, body = md_to_html("# T\nHello world")
assert "<p>Hello world</p>" in body
def test_page_wraps_body():
"""The template includes the body and title."""
result = page("Title", "<p>Hi</p>")
assert "<title>Title</title>" in result
assert "<p>Hi</p>" in result
Run with pytest -v. We test the conversion and templating as pure functions — no files touched. The file-writing in build just glues these tested pieces together.
5 The Interface
What it expects
posts/hello.md:
# Hello World
My first post.What it returns
site/hello.html
site/index.html (links to all posts)6 Run It & Automate It
python3 blog.pyPut
.md files in a posts/ folder, run it, and open site/index.html in a browser.Jenkins runs the tests automatically — each line explained below.
Site built in ./site
# site/ now contains one .html per post plus an index.html linking them allFileNotFoundError on the posts folder.posts/ folder with some .md files first..md files. Check the folder and order.// Jenkinsfile — automated tests on every change.
pipeline {
agent any
stages {
stage('Get the code') { steps { checkout scm } } // download the code
stage('Install') { steps { sh 'python3 -m venv venv && . venv/bin/activate && pip install pytest' } } // test tool
stage('Test') { steps { sh '. venv/bin/activate && pytest -v' } } // run every test
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
- Add styling. Include a CSS file in the template. (Teaches: richer templates.)
- Dates and sorting. Show posts newest-first. (Teaches: metadata and sorting.)
- Full Markdown. Support bold, links, and lists. (Teaches: extending the parser.)