Building a Static Blog With Python and Markdown
A static blog is one of the best small projects for learning how websites are really assembled. It sounds simple:
Take Markdown files and turn them into HTML pages. But that small idea touches a lot of useful concepts:
- content modeling
- frontmatter
- templates
- routing
- pagination
- categories
- static assets
- build scripts
- deployment That is why I like this kind of project. It is practical, but it is still small enough to understand.
The Mental Model
The core pipeline looks like this: ```plain text Markdown files -> parser -> templates -> HTML files
For this site, the larger shape is:
```plain text
Notion or local Markdown -> content/blog -> Python build -> output/
The public web server does not need to know about Notion, Markdown parsing, Sass, or image optimization. It only needs to serve the generated files in `output/`. That is the main advantage of static generation.
Markdown as Content
Markdown is a good authoring format because it stays close to plain text. A blog post can look like this:
---
title: "My Post"
slug: "my-post"
category: "Projects"
tags:
- "python"
- "static-site"
---
# My Post
This is the article body.
The top section is frontmatter. The body is Markdown. Frontmatter stores metadata the build system needs:
- title
- slug
- category
- tags
- description
- date
- image The Markdown body stores the article content.
Parsing Content
The Python build step needs to read each post, parse the frontmatter, convert the Markdown body into HTML, and pass the result into a template. Conceptually:
post = read_markdown_file(path)
metadata = post.frontmatter
html = render_markdown(post.body)
page = render_template("blog_post.html", post=post, html=html)
write_output(page)
The actual project can have more details, but the pipeline is still understandable. That is one reason I like custom static generators for personal sites. You can keep the system close to the shape of your own content.
Templates
Templates keep HTML structure reusable. A blog post page does not need to duplicate the whole site layout in every file. Instead, the build system can use templates: ```plain text base.html blog_post.html blog-list.html navbar.html footer.html
The post template handles the article.
The base template handles the shared page structure.
Partials handle repeated pieces like navigation and footer.
This keeps design changes manageable.
If the header changes, you should not need to edit every generated blog post by hand.
## Blog Indexes and Categories
A blog is more than individual posts.
It usually needs:
- blog index page
- category pages
- pagination
- tag or topic metadata
- RSS feed if you want syndication later
The static generator can build these pages from the same source content.
For example:
```plain text
content/blog/python-post.md
content/blog/git-post.md
content/blog/static-site-post.md
The build step can create:
plain text
output/blog/index.html
output/blog/python-post/index.html
output/blog/git-post/index.html
output/blog/category/projects/index.html
The content files are the source of truth. The generated pages are output.
Assets
Static sites still need asset handling. Common assets include:
- source images
- generated optimized images
- Sass or CSS
- TypeScript or JavaScript
- icons
- fonts The important distinction is source versus generated output. Source assets should exist in the repository when the site depends on them. Generated assets should be reproducible. For example, original portfolio icons might live under source assets, while optimized copies are written to `output/` during the build.
Local Development
The local development loop should be short:
- Edit content, templates, styles, or assets.
- Rebuild the site.
- Refresh the browser. In a better setup, a watcher handles rebuilds and BrowserSync reloads the page. That does not change the core architecture. It just makes development less annoying. The production build should still be able to run cleanly from the command line.
Common Mistakes
Mistake 1: Letting generated output become the source of truth
Generated HTML should be replaceable. The real source is content, templates, and assets.
Mistake 2: Hiding too much in the build system
A custom generator should stay readable. If changing the blog layout requires reverse engineering the whole build, the system is too clever.
Mistake 3: Forgetting the second machine test
A static blog should be easy to clone and build elsewhere. If it depends on private local paths, global packages, or missing source assets, the project is not portable yet.
Where This Shows Up in Real Projects
This site uses the static blog pattern because it fits the job. The content can be written in Notion or Markdown. The build step turns it into static pages. The generated `output/` directory is what production serves. That keeps the public site simple while still allowing a useful authoring workflow. It also makes the site easier to reason about:
- content lives in content files
- templates control layout
- build tools create output
- deployment serves static files That separation is boring in the best way.
Key Takeaways
- A static blog turns content files into generated HTML.
- Markdown plus frontmatter is a simple content format.
- Templates keep page structure reusable.
- Generated output should not become the source of truth.
A good static site should build cleanly on another machine.
Related Articles
Rebuilding tristanisfeld.com
- How to Structure a Python Project
- Terminal Tools I Actually Use