1 The Problem
We want a real static-site generator: Markdown content files combined with a reusable HTML layout (header, nav, footer), producing a complete styled website. It builds on the Markdown blog with proper templating and layouts — the architecture of Jekyll, Hugo, and Eleventy.
2 How to Think About It
Think about content plus layout, before any code:
{content} placeholder and shared parts (nav, footer). → 2. Each content file is converted to HTML. → 3. Combine: drop each page’s content into the layout, so every page shares the same look. → 4. Build an index and write everything out. Separating content from layout is the whole idea.
3 The Build — explained part by part
Here is the complete generator with layouts. Each part is explained below.
from pathlib import Path
LAYOUT = """<!DOCTYPE html>
<html><head><title>{title}</title></head>
<body>
<nav><a href="index.html">Home</a></nav>
<main>{content}</main>
<footer>Built with my SSG</footer>
</body></html>"""
def md_to_html(text):
title = "Untitled"
html = []
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 render(title, content):
# Drop the content into the shared layout.
return LAYOUT.format(title=title, content=content)
def build(content_dir="content", out_dir="public"):
out = Path(out_dir)
out.mkdir(exist_ok=True)
index = []
for md in Path(content_dir).glob("*.md"):
title, body = md_to_html(md.read_text())
page = render(title, body)
name = md.stem + ".html"
(out / name).write_text(page)
index.append(f'<li><a href="{name}">{title}</a></li>')
index_html = render("Home", "<ul>" + "".join(index) + "</ul>")
(out / "index.html").write_text(index_html)
if __name__ == "__main__":
build()
print("Site built in ./public")
{title} and {content} placeholders for the bits that differ. This is what gives a site a consistent look.md_to_html — convert a content file to HTML and extract its title (reused from the blog project).
render(title, content) — the templating step:
LAYOUT.format(...) drops this page’s title and content into the shared layout. Content and layout, combined.build — the pipeline: for each Markdown file, convert, render through the layout, and write. Then build an index page — itself rendered through the same layout, so it matches.
the key idea — content (Markdown) is separate from presentation (layout). Change the layout once and every page updates. That separation is why static-site generators are so powerful.
{ in the layout — .format tries to interpret it.{{ and }} in format strings.4 Test & Prove Each Part
We test the conversion and the layout rendering — that content lands inside the shared template.
LAYOUT = """<html><head><title>{title}</title></head>
<body><nav>Home</nav><main>{content}</main><footer>Footer</footer></body></html>"""
def md_to_html(text):
title = "Untitled"
html = []
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 render(title, content):
return LAYOUT.format(title=title, content=content)
def test_markdown_conversion():
"""The # line becomes the title and an h1."""
title, body = md_to_html("# Welcome\nHello")
assert title == "Welcome"
assert "<h1>Welcome</h1>" in body
def test_layout_wraps_content():
"""The layout includes nav, footer, and the content."""
page = render("T", "<p>Hi</p>")
assert "<nav>" in page
assert "<footer>" in page
assert "<p>Hi</p>" in page
def test_title_in_page():
"""The page title is set from the content."""
page = render("My Page", "body")
assert "<title>My Page</title>" in page
Run with pytest -v. The layout-wraps-content test proves the templating works: content ends up inside the shared structure. Test conversion and layout separately, and the full build is just gluing tested pieces.
5 The Interface
content/about.md
# About
We build things.public/about.html
full page with nav, content, footer6 Run It & Automate It
python3 ssg.pyPut
.md files in content/, run it, open public/index.html. Edit the layout to restyle every page at once.Jenkins runs the tests automatically — each line explained below.
Site built in ./public
# public/ has a styled page per content file, all sharing the layout, plus index.htmlKeyError during render.{word} in the layout is treated as a placeholder. Escape literal braces.render, not just the raw converted HTML.// 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.' }
}
}
- CSS file. Link a stylesheet in the layout. (Teaches: assets.)
- Front matter. Read metadata (date, tags) from the top of each file. (Teaches: parsing metadata.)
- Multiple layouts. Different templates for posts vs pages. (Teaches: layout selection.)