Updated Jul 6, 2026

What a Generator Actually Is, and Setup

Before writing any code, you need the one mental model the entire project rests on. It's small, and once it clicks, every static site tool you'll ever meet - Jekyll, Hugo, Eleventy - stops being mysterious.

Build time vs request time

What a dynamic site does. When someone visits a WordPress blog, a program wakes up at that moment: it queries a database for the post, runs it through a theme, assembles HTML, and sends it back. That work happens on every single request, which is why a dynamic site needs a server running code 24/7, a database, security patches, and a hosting bill.

What a static site generator does. It does the exact same assembly work - read content, apply a template, produce HTML - but once, on your machine, before anything is deployed. The output is a folder of finished .html files. The web host's only job is handing those files out, which is the one thing web servers are effortlessly good at.

That's the whole idea. The trade is explicit: you give up per-visitor behavior (comments, logins, "personalized for you") and in exchange the site is fast, free to host, and has nothing running that can crash or be hacked.

💡 Key point: a static site generator is not a kind of website. It's a program that runs at build time and writes a website out as files. build.py, the script you're about to write, is a static site generator - the same species as Hugo, just smaller.

📝 Terminology: the moment your generator runs is build time. The moment a visitor loads a page is request time. At request time, a static site does nothing at all - the work was already done.

Make the project

Open a terminal, make a folder, and set up an isolated Python environment inside it:

mkdir myblog
cd myblog
python -m venv .venv

python -m venv .venv creates a virtual environment - a private copy of Python in the .venv folder, so the library we install lands in this project instead of polluting your system Python. Activate it:

# macOS / Linux:
source .venv/bin/activate

# Windows (PowerShell or cmd):
.venv\Scripts\activate

Your prompt grows a (.venv) prefix - that's how you know pip now installs into the project. You'll need to re-activate in every new terminal you open for this project.

Now install the one dependency:

(.venv) $ pip install markdown
Collecting markdown
  Downloading markdown-3.8.2-py3-none-any.whl (106 kB)
Installing collected packages: markdown
Successfully installed markdown-3.8.2

Your version number may differ - that's fine. The markdown package converts Markdown text to HTML. That's the only job we're outsourcing: writing a correct Markdown parser is a multi-week project of its own, and it isn't the thing this build teaches.

Create the folders the generator will read from:

mkdir posts templates static

Which gives you this layout:

myblog/
  .venv/         <- the virtual environment (never touch it)
  build.py       <- the generator - you write this next
  posts/         <- your writing, one .md file per post
  templates/     <- HTML skeletons (phase 3)
  static/        <- CSS, copied through untouched (phase 3)
  site/          <- the OUTPUT - generated, never edited by hand

site/ doesn't exist yet. The build creates it - and that's a rule worth tattooing somewhere: you never edit anything in site/, because the next build will overwrite it. Source lives in posts/, templates/, and static/; everything in site/ is disposable.

Write two posts

A generator with nothing to generate is hard to test. Posts are Markdown files with a metadata block at the top. Create posts/2026-06-28-hello-world.md:

---
title: Hello, World
date: 2026-06-28
tags: meta
---

This is the first post on a blog generated by a Python script I wrote
myself. It is plain **Markdown** in a plain text file.

Things this blog does not have:

- a database
- an admin panel
- a monthly bill

And a second one, posts/2026-07-02-why-static-sites.md:

---
title: Why I Went Static
date: 2026-07-02
tags: meta, web
---

My last blog ran on a database, a PHP runtime, and a prayer. This one
is a folder of files.

Static means there is nothing to patch on a Tuesday night, and nothing
that falls over when a post gets popular.

```python
# nothing stops me from putting code in a post, either
print("hello from a code block")
```

The block between the --- lines is called frontmatter - metadata about the post that isn't part of the post. The filename starts with the date so posts sort naturally in a file listing; the rest of the filename will become the post's URL later.

📝 Terminology: frontmatter is the key: value block fenced by --- at the top of a content file. Jekyll popularized it, and now it's everywhere - this platform's own guides use it. You'll write the parser for it in phase 2.

The first build.py

Start the generator with the smallest thing that proves the pipeline: find the posts. Create build.py in the project root:

from pathlib import Path

POSTS_DIR = Path("posts")


def main():
    posts = sorted(POSTS_DIR.glob("*.md"))
    print(f"Found {len(posts)} post(s):")
    for path in posts:
        print(" -", path.name)


if __name__ == "__main__":
    main()

Two things here carry through the whole project. Path("posts") is pathlib, the standard library's way of handling files - POSTS_DIR.glob("*.md") means "every .md file in that folder," and it returns them in no guaranteed order, which is why we sorted() them. And the if __name__ == "__main__": guard means "only run main() when this file is executed directly, not when it's imported" - that guard becomes important in phase 5, when another script imports this one.

Run it:

(.venv) $ python build.py
Found 2 post(s):
 - 2026-06-28-hello-world.md
 - 2026-07-02-why-static-sites.md

What just happened: Python globbed the posts/ folder, found your two files, and listed them. Unimpressive on purpose - but the pipeline's first segment (source folder → script) is real and verified. Everything from here on is teaching this script what to do with those files.

What you have now

A project folder with an isolated environment, one installed library, two honest Markdown posts, and a script that finds them. Next phase, the script learns to actually read a post: split the frontmatter from the body, parse the metadata, and convert the Markdown to HTML - the input half of the generator, done properly.

Quick check that the mental model stuck before you move on:

[
  {
    "q": "When does a static site generator do its work?",
    "choices": ["On every visitor request, like WordPress", "Once, at build time, before anything is deployed", "Continuously, on a schedule in the cloud"],
    "answer": 1,
    "explain": "The generator runs once on your machine and writes finished HTML files. At request time, the host serves those files as-is - no code runs per visitor."
  },
  {
    "q": "What will the site/ folder hold, and how should you treat it?",
    "choices": ["Your Markdown sources - edit freely", "Server configuration files", "The generated output - never edit it by hand, the next build overwrites it"],
    "answer": 2,
    "explain": "site/ is disposable output. Source of truth lives in posts/, templates/, and static/."
  },
  {
    "q": "What is frontmatter?",
    "choices": ["The key: value metadata block between --- lines at the top of a content file", "The first paragraph of a post", "An HTML template header"],
    "answer": 0,
    "explain": "It carries data about the post (title, date, tags) that isn't part of the post's body text."
  }
]
Check your understanding 3 questions

1. When does a static site generator do its work?

2. What will the site/ folder hold, and how should you treat it?

3. What is frontmatter?