thecodex.expert · The Codex Family of Knowledge
Tier 4 · Advanced · Python Project

Static Site Generator

A full static-site generator with templates, a layout, and Markdown content — a more powerful version of the blog project. Build your own Jekyll.

🧠 Teaches how to think spoonfed, every age Last verified:

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.

Where this shows up: Jekyll, Hugo, Eleventy, Next.js static export, every documentation site and blog built from Markdown. Understanding layouts and templating is key to modern web content workflows.

2 How to Think About It

Think about content plus layout, before any code:

The plan — in plain English
1. A layout is an HTML template with a {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.

Layout template with placeholder

Combine

Markdown content file

Convert to HTML

Full styled page

Write to output

Build index of all pages

3 The Build — explained part by part

Here is the complete generator with layouts. Each part is explained below.

Pythonssg.py
from pathlib import Path

LAYOUT = &quot;&quot;&quot;<!DOCTYPE html>
<html><head><title>{title}</title></head>
<body>
  <nav><a href=&quot;index.html&quot;>Home</a></nav>
  <main>{content}</main>
  <footer>Built with my SSG</footer>
</body></html>&quot;&quot;&quot;

def md_to_html(text):
    title = &quot;Untitled&quot;
    html = []
    for line in text.splitlines():
        if line.startswith(&quot;# &quot;):
            title = line[2:]
            html.append(f&quot;<h1>{title}</h1>&quot;)
        elif line.strip():
            html.append(f&quot;<p>{line}</p>&quot;)
    return title, &quot;\n&quot;.join(html)

def render(title, content):
    # Drop the content into the shared layout.
    return LAYOUT.format(title=title, content=content)

def build(content_dir=&quot;content&quot;, out_dir=&quot;public&quot;):
    out = Path(out_dir)
    out.mkdir(exist_ok=True)
    index = []
    for md in Path(content_dir).glob(&quot;*.md&quot;):
        title, body = md_to_html(md.read_text())
        page = render(title, body)
        name = md.stem + &quot;.html&quot;
        (out / name).write_text(page)
        index.append(f&#x27;<li><a href=&quot;{name}&quot;>{title}</a></li>&#x27;)
    index_html = render(&quot;Home&quot;, &quot;<ul>&quot; + &quot;&quot;.join(index) + &quot;</ul>&quot;)
    (out / &quot;index.html&quot;).write_text(index_html)

if __name__ == &quot;__main__&quot;:
    build()
    print(&quot;Site built in ./public&quot;)
What each part does — in plain words
LAYOUT — the shared template: every page gets the same nav and footer, with {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.
Common mistakes — and how to avoid them
✗ Repeating the HTML boilerplate in every page — impossible to restyle.
✓ Use one shared layout with placeholders.
✗ Putting a literal { in the layout — .format tries to interpret it.
✓ Escape literal braces as {{ and }} in format strings.
✗ Rendering the index without the layout — it looks different from other pages.
✓ Render the index through the same layout for consistency.

4 Test & Prove Each Part

We test the conversion and the layout rendering — that content lands inside the shared template.

Markdown converts to HTML with a title
The layout wraps content with nav and footer
The title appears in the rendered page
Pythontest_ssg.py
LAYOUT = &quot;&quot;&quot;<html><head><title>{title}</title></head>
<body><nav>Home</nav><main>{content}</main><footer>Footer</footer></body></html>&quot;&quot;&quot;


def md_to_html(text):
    title = &quot;Untitled&quot;
    html = []
    for line in text.splitlines():
        if line.startswith(&quot;# &quot;):
            title = line[2:]
            html.append(f&quot;<h1>{title}</h1>&quot;)
        elif line.strip():
            html.append(f&quot;<p>{line}</p>&quot;)
    return title, &quot;\n&quot;.join(html)


def render(title, content):
    return LAYOUT.format(title=title, content=content)


def test_markdown_conversion():
    &quot;&quot;&quot;The # line becomes the title and an h1.&quot;&quot;&quot;
    title, body = md_to_html(&quot;# Welcome\nHello&quot;)
    assert title == &quot;Welcome&quot;
    assert &quot;<h1>Welcome</h1>&quot; in body


def test_layout_wraps_content():
    &quot;&quot;&quot;The layout includes nav, footer, and the content.&quot;&quot;&quot;
    page = render(&quot;T&quot;, &quot;<p>Hi</p>&quot;)
    assert &quot;<nav>&quot; in page
    assert &quot;<footer>&quot; in page
    assert &quot;<p>Hi</p>&quot; in page


def test_title_in_page():
    &quot;&quot;&quot;The page title is set from the content.&quot;&quot;&quot;
    page = render(&quot;My Page&quot;, &quot;body&quot;)
    assert &quot;<title>My Page</title>&quot; 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

INPUTcontent/ + layoutMarkdown + template
content/about.md
# About
We build things.
OUTPUTpublic/styled site with nav + index
public/about.html
full page with nav, content, footer

6 Run It & Automate It

Run it locally
python3 ssg.py
Put .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.

What you should see when it works
Terminala real run
Site built in ./public
# public/ has a styled page per content file, all sharing the layout, plus index.html
If it breaks — how to fix it
🚨 KeyError during render.
A stray {word} in the layout is treated as a placeholder. Escape literal braces.
🚨 Pages have no styling.
Make sure each page goes through render, not just the raw converted HTML.
GroovyJenkinsfile
// Jenkinsfile — automated tests on every change.
pipeline {
    agent any
    stages {
        stage(&#x27;Get the code&#x27;) { steps { checkout scm } }       // download the code
        stage(&#x27;Install&#x27;) { steps { sh &#x27;python3 -m venv venv && . venv/bin/activate && pip install pytest&#x27; } }  // test tool
        stage(&#x27;Test&#x27;) { steps { sh &#x27;. venv/bin/activate && pytest -v&#x27; } }   // run every test
    }
    post {
        success { echo &#x27;All tests passed.&#x27; }
        failure { echo &#x27;A test failed — look above.&#x27; }
    }
}
🎯 Try this next — make it yours
  1. CSS file. Link a stylesheet in the layout. (Teaches: assets.)
  2. Front matter. Read metadata (date, tags) from the top of each file. (Teaches: parsing metadata.)
  3. Multiple layouts. Different templates for posts vs pages. (Teaches: layout selection.)
What you learned
You built a real static-site generator with layouts — separating content from presentation so one template styles every page. This is the architecture of Jekyll, Hugo, and Eleventy. Related: pathlib, f-strings.