Skip to main content

Static site generation with Flask: building a simple blog

Learn how to create a lightweight static blog using Flask and Flask-FlatPages with Markdown content.

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 .md extension
  • Find content in the content directory
  • 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>&copy; {{ 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!