← 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"<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")
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 = "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

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('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.' }
    }
}
🎯 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.