thecodex.expert · The Codex Family of Knowledge
Tier 2 · Intermediate · Python Project

Markdown Blog Generator

Turn a folder of Markdown posts into a complete static website with an index page. A mini static-site generator.

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

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.

Where this shows up: static-site generators (Jekyll, Hugo, Eleventy), documentation tools, blog engines, GitHub Pages. The pattern — read source files, transform each, build an index — is exactly how this very site could be built.

2 How to Think About It

Think about the build pipeline, before any code:

The plan — in plain English
1. Find every Markdown file in the posts folder. → 2. For each, convert it to HTML and wrap it in a page template. → 3. Collect the titles and filenames. → 4. Build an index page linking to all posts. → 5. Write everything to an output folder.

Find markdown posts

For each post

Convert to HTML

Wrap in page template

Write HTML file

Record title + link

Build index page

Write index

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.

Pythonblog.py
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&quot;<h1>{title}</h1>&quot;)
        elif line.strip():
            html.append(f&quot;<p>{line}</p>&quot;)
    return title, &quot;\n&quot;.join(html)

def page(title, body):
    return f&quot;<!DOCTYPE html><html><head><title>{title}</title></head><body>{body}</body></html>&quot;

def build(posts_dir=&quot;posts&quot;, out_dir=&quot;site&quot;):
    posts = Path(posts_dir)
    out = Path(out_dir)
    out.mkdir(exist_ok=True)

    index_links = []
    for md_file in posts.glob(&quot;*.md&quot;):
        title, body = md_to_html(md_file.read_text())
        html_name = md_file.stem + &quot;.html&quot;
        (out / html_name).write_text(page(title, body))
        index_links.append(f&#x27;<li><a href=&quot;{html_name}&quot;>{title}</a></li>&#x27;)

    index_body = &quot;<h1>Blog</h1><ul>&quot; + &quot;&quot;.join(index_links) + &quot;</ul>&quot;
    (out / &quot;index.html&quot;).write_text(page(&quot;Blog&quot;, index_body))

if __name__ == &quot;__main__&quot;:
    build()
    print(&quot;Site built in ./site&quot;)
What each part does — in plain words
md_to_html(text) — convert one post: the first # 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.
Common mistakes — and how to avoid them
✗ Forgetting to create the output folder — writing files fails.
✓ Call out.mkdir(exist_ok=True) before writing.
✗ Overwriting posts because two files share a stem — a.md and a.markdown both make a.html.
✓ Use unique post filenames, or include more of the name.
✗ Building the index before collecting all links — it comes out empty.
✓ Collect links during the loop, build the index after.

4 Test & Prove Each Part

We test the conversion and templating — the pure logic — without writing real files.

The first # line becomes the title
Body lines become paragraphs
The page template wraps the body in HTML
Pythontest_blog.py
def md_to_html(text):
    html = []
    title = &quot;Untitled&quot;
    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 page(title, body):
    return f&quot;<!DOCTYPE html><html><head><title>{title}</title></head><body>{body}</body></html>&quot;


def test_title_extracted():
    &quot;&quot;&quot;The # line becomes the title.&quot;&quot;&quot;
    title, _ = md_to_html(&quot;# My Post\nHello&quot;)
    assert title == &quot;My Post&quot;


def test_paragraph():
    &quot;&quot;&quot;A body line becomes a paragraph.&quot;&quot;&quot;
    _, body = md_to_html(&quot;# T\nHello world&quot;)
    assert &quot;<p>Hello world</p>&quot; in body


def test_page_wraps_body():
    &quot;&quot;&quot;The template includes the body and title.&quot;&quot;&quot;
    result = page(&quot;Title&quot;, &quot;<p>Hi</p>&quot;)
    assert &quot;<title>Title</title>&quot; in result
    assert &quot;<p>Hi</p>&quot; 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

INPUTposts/ folderMarkdown files
What it expects
posts/hello.md:
# Hello World
My first post.
OUTPUTsite/ folderHTML pages + index
What it returns
site/hello.html
site/index.html  (links to all posts)

6 Run It & Automate It

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

What you should see when it works
Terminala real run
Site built in ./site
# site/ now contains one .html per post plus an index.html linking them all
If it breaks — how to fix it
🚨 FileNotFoundError on the posts folder.
Create a posts/ folder with some .md files first.
🚨 The index is empty.
You built it before the loop, or the loop found no .md files. Check the folder and order.
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. Add styling. Include a CSS file in the template. (Teaches: richer templates.)
  2. Dates and sorting. Show posts newest-first. (Teaches: metadata and sorting.)
  3. Full Markdown. Support bold, links, and lists. (Teaches: extending the parser.)
What you learned
You built a real static-site generator: a build pipeline that reads source files, transforms each through a template, and assembles an index. This combines parsing, file I/O, and templating — and is exactly how modern blogs and docs sites work. Related: pathlib, f-strings.