Introduction
Static site generators have become increasingly popular due to their simplicity, security, and performance benefits. While there are many dedicated static site generators available, Flask—a lightweight Python web framework—can be transformed into an elegant static site generator with minimal effort.
In this guide, we'll build a simple yet powerful blog system using Flask and Flask-FlatPages. This approach gives you the flexibility of a dynamic Flask application during development, with the option to freeze it into static files for production.
What we'll build
- A blog system that uses Markdown files with YAML frontmatter
- A clean, organised project structure using the application factory pattern
- A simple but functional blog homepage that lists posts chronologically
- Individual post pages with rendered Markdown content
Prerequisites
To follow along, you should have:
- Basic knowledge of Python and Flask
- Python 3.6+ installed
- Familiarity with the command line
- Understanding of basic HTML and Markdown
Project setup
Let's start by creating our project structure and installing the necessary dependencies.
# Create project directory
mkdir -p flask_ssg
cd flask_ssg
# Set up virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install flask flask-flatpages markdown pygments frozen-flask
The packages we're installing are:
- Flask: Our web framework
- Flask-FlatPages: For handling Markdown content as pages
- Markdown: For parsing Markdown files
- Pygments: For syntax highlighting in code blocks
- Frozen-Flask: For freezing the app into static files (optional for deployment)
Project structure
Let's set up our project with a clean structure following Flask best practices:
flask_ssg/
├── app/
│ ├── __init__.py
│ ├── routes.py
│ ├── templates/
│ │ ├── base.html
│ │ ├── blog_post.html
│ │ └── blog_home.html
│ └── static/
│ └── css/
│ └── style.css
├── content/
│ └── blog/
│ ├── hello-world.md
│ └── flask-introduction.md
├── config.py
└── run.py
This structure separates our application code from the content, making it easier to maintain and extend.
Creating the application factory
Let's start by implementing the application factory pattern. This approach makes our application more modular and easier to test.
Create the app/__init__.py file:
from flask import Flask
from flask_flatpages import FlatPages
# Initialize extensions
pages = FlatPages()
def create_app(config_object='config.Config'):
# Create Flask app
app = Flask(__name__)
# Load configuration
app.config.from_object(config_object)
# Initialize extensions with app
pages.init_app(app)
# Register blueprints
from app.routes import blog_bp
app.register_blueprint(blog_bp)
return app
Configuration
Now let's create our configuration file config.py:
class Config:
# Flask-FlatPages configuration
FLATPAGES_AUTO_RELOAD = True
FLATPAGES_EXTENSION = '.md'
FLATPAGES_ROOT = 'content'
FLATPAGES_MARKDOWN_EXTENSIONS = ['codehilite', 'fenced_code', 'tables']
# General Flask configuration
DEBUG = True
SECRET_KEY = 'development-key' # Change this in production
These settings configure Flask-FlatPages to:
- Automatically reload pages when they're modified (useful during development)
- Look for files with the
.mdextension - Find content in the
contentdirectory - Use additional Markdown extensions for richer content
Creating routes
Now let's create our blog routes in app/routes.py:
from datetime import datetime
from flask import Blueprint, render_template, abort
from app import pages
blog_bp = Blueprint('blog', __name__)
@blog_bp.route('/')
def index():
"""Blog home page with list of posts"""
# Get all pages with a date in their metadata
blog_posts = [p for p in pages if 'date' in p.meta]
# Sort pages by date, newest first
sorted_posts = sorted(
blog_posts,
reverse=True,
key=lambda page: datetime.strptime(
page.meta['date'],
'%Y-%m-%d'
) if isinstance(page.meta['date'], str) else page.meta['date']
)
return render_template('blog_home.html', posts=sorted_posts)
@blog_bp.route('/<path:path>/')
def post(path):
"""Individual blog post page"""
blog_post = pages.get_or_404(f'blog/{path}')
return render_template('blog_post.html', post=blog_post)
This creates two routes:
/- The blog homepage showing a list of all posts/<path>/- Individual blog post pages
Creating templates
Now let's create our templates. First, the base template that others will extend:
app/templates/base.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Flask Blog{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
{% block extra_head %}{% endblock %}
</head>
<body>
<header>
<nav>
<a href="{{ url_for('blog.index') }}">Blog Home</a>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>© {{ now.year }} Flask Static Blog</p>
</footer>
</body>
</html>
app/templates/blog_home.html
{% extends 'base.html' %}
{% block title %}Blog Home{% endblock %}
{% block content %}
<h1>Blog Posts</h1>
<div class="post-list">
{% for post in posts %}
<article class="post-summary">
<h2>
<a href="{{ url_for('blog.post', path=post.path.replace('blog/', '')) }}">
{{ post.meta.title }}
</a>
</h2>
<div class="post-meta">
<time datetime="{{ post.meta.date }}">
{{ post.meta.date }}
</time>
{% if post.meta.tags %}
<div class="tags">
{% for tag in post.meta.tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
</div>
{% if post.meta.summary %}
<p class="summary">{{ post.meta.summary }}</p>
{% endif %}
</article>
{% endfor %}
</div>
{% endblock %}
app/templates/blog_post.html
{% extends 'base.html' %}
{% block title %}{{ post.meta.title }}{% endblock %}
{% block content %}
<article class="post">
<header>
<h1>{{ post.meta.title }}</h1>
<div class="post-meta">
<time datetime="{{ post.meta.date }}">
{{ post.meta.date }}
</time>
{% if post.meta.tags %}
<div class="tags">
{% for tag in post.meta.tags %}
<span class="tag">{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
</div>
</header>
<div class="post-content">
{{ post.html|safe }}
</div>
</article>
{% endblock %}
Adding some basic styles
Let's add a minimal stylesheet to make our blog presentable:
app/static/css/style.css
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 1rem;
}
a {
color: #0066cc;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Blog post list */
.post-summary {
margin-bottom: 2rem;
border-bottom: 1px solid #eee;
padding-bottom: 1rem;
}
.post-meta {
color: #666;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.tag {
background-color: #f0f0f0;
padding: 0.2rem 0.5rem;
border-radius: 3px;
font-size: 0.8rem;
margin-right: 0.5rem;
}
/* Code highlighting */
pre {
background-color: #f5f5f5;
padding: 1rem;
overflow-x: auto;
border-radius: 4px;
}
code {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 0.9em;
}
Creating blog content
Now let's create some sample blog posts. We'll start with a hello world post:
content/blog/hello-world.md
---
title: "Hello world: getting started with our Flask blog"
date: 2025-03-01
tags: ["flask", "introduction"]
summary: "Our first blog post explaining the setup and features of our Flask static blog."
---
# Hello World!
Welcome to our Flask-powered static blog. This post will walk you through the basic features of our blog system.
## Markdown support
This blog supports all standard Markdown features:
- **Bold text** and *italic text*
- Lists (like this one)
- [Links](https://flask.palletsprojects.com/)
- And much more!
## Code syntax highlighting
```python
@app.route('/')
def hello_world():
return 'Hello, Flask!'
What's next?
In upcoming posts, we'll explore more advanced features and customizations.
### `content/blog/flask-introduction.md`
```markdown
---
title: "Introduction to Flask: a lightweight Python web framework"
date: 2023-05-26
tags: ["flask", "python", "web-development"]
summary: "Learn about Flask, a minimalist Python web framework perfect for small to medium projects."
---
# Introduction to Flask
Flask is a lightweight WSGI web application framework in Python. It's designed to make getting started quick and easy, with the ability to scale up to complex applications.
## Key features of Flask
- **Simplicity**: Flask's core is simple but extensible
- **Flexibility**: Few decisions are made for you
- **Extensions**: Rich ecosystem of extensions for added functionality
- **Jinja2 templating**: Powerful and flexible templating system
## A simple Flask application
```python
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello, World!'
if __name__ == '__main__':
app.run(debug=True)
Just save this as app.py and run it with python app.py!
## Running the application
Finally, let's create the entry point for our application, `run.py`:
```python
from app import create_app
from datetime import datetime
app = create_app()
# Add template context processor to provide current date to all templates
@app.context_processor
def inject_now():
return {'now': datetime.now()}
if __name__ == '__main__':
app.run(debug=True)
Now you can run your blog with:
python run.py
Visit http://localhost:5000/ in your browser to see your blog!
Generating content programmatically
If you need to generate multiple blog posts quickly for testing, you can use this shell script:
#!/bin/bash
# Create content directory if it doesn't exist
mkdir -p content/blog
# Generate 4 sample posts using Lorem Markdownum
for i in {1..4}; do
POST_PATH="content/blog/sample-post-$i.md"
DAYS_AGO=$((i * 3))
DATE=$(date -d "-$DAYS_AGO days" +"%Y-%m-%d")
# Create frontmatter
echo "---" > $POST_PATH
echo "title: \"Sample blog post $i\"" >> $POST_PATH
echo "date: $DATE" >> $POST_PATH
echo "tags: [\"sample\", \"lorem-ipsum\"]" >> $POST_PATH
echo "summary: \"A sample blog post generated with Lorem Markdownum.\"" >> $POST_PATH
echo "---" >> $POST_PATH
echo "" >> $POST_PATH
# Fetch Lorem Markdownum content and append to file
curl "https://jaspervdj.be/lorem-markdownum/markdown.txt?fenced-code-blocks=on" >> $POST_PATH
done
echo "Generated 4 sample blog posts in content/blog/"
Next steps
This Flask-based blog system is simple but powerful. Here are some ways you could extend it:
- Add a category system for organizing posts
- Implement tagging functionality with dedicated tag pages
- Add pagination for the blog index
- Create an RSS feed for your blog
- Add search functionality
- Integrate with a static file generator like Frozen-Flask to deploy your site
- Add a commenting system (using a third-party service or a custom solution)
Conclusion
You've now built a simple but functional static blog system using Flask and Flask-FlatPages. This approach gives you the best of both worlds: the simplicity of working with static files and the power of a Flask application.
The beauty of this approach is that you can use familiar tools like Python and Flask to manage your content, while still having the option to deploy a fast, secure static site. As your blog grows, you can easily extend it with additional features to meet your needs.
Happy blogging with Flask!