Python Flask Blog Application Tutorial

Written by

in

Looking to build a dynamic, lightweight blog with Python? This step‑by‑step Python Flask blog application tutorial will guide you from a fresh virtual environment to a fully functional, database‑backed blog that you can deploy on any cloud provider. By the end of this guide you’ll understand the core Flask concepts, how to structure a scalable project, and how to add essential features like user authentication, markdown support, and pagination—all while keeping SEO best practices in mind.

Why Choose Flask for a Blog?

Flask is a micro‑framework that gives you the flexibility to pick the tools you need without the overhead of a full‑stack solution. This makes it perfect for a blog where you want:

  • Lightweight performance – minimal dependencies, fast response times.
  • Full control over routing, templates, and database layers.
  • Easy scalability – you can start with SQLite and later migrate to PostgreSQL or MySQL.
  • Great community support – countless extensions for forms, authentication, and more.

Project Setup: Getting the Foundations Right

1. Create a virtual environment

python3 -m venv venv
source venv/bin/activate  # On Windows use `venv\Scripts\activate`

2. Install Flask and essential extensions

pip install Flask Flask-WTF Flask-Login Flask-Migrate Flask-Markdown
pip install gunicorn  # For production

3. Directory structure

Organize your files so the project remains maintainable as it grows:

my_blog/
│
├── app/
│   ├── __init__.py      # Application factory
│   ├── models.py        # Database models
│   ├── routes.py        # View functions
│   ├── forms.py         # WTForms definitions
│   ├── templates/
│   │   ├── base.html
│   │   ├── index.html
│   │   └── post.html
│   └── static/
│       └── style.css
│
├── migrations/          # Flask‑Migrate files
├── config.py            # Configuration classes
└── run.py               # Entry point

Creating the Flask Application Factory

The factory pattern lets you create multiple instances of the app (useful for testing). In app/__init__.py add:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_migrate import Migrate
from flask_markdown import Markdown

db = SQLAlchemy()
login_manager = LoginManager()
migrate = Migrate()
markdown = Markdown()

def create_app(config_class='config.DevelopmentConfig'):
    app = Flask(__name__)
    app.config.from_object(config_class)

    # Initialise extensions
    db.init_app(app)
    login_manager.init_app(app)
    migrate.init_app(app, db)
    markdown.init_app(app)

    # Register blueprints (optional but recommended)
    from .routes import main
    app.register_blueprint(main)

    return app

Defining the Data Model

Our blog needs at least two models: User and Post. Add the following to app/models.py:

from datetime import datetime
from . import db, login_manager
from flask_login import UserMixin

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(150), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password_hash = db.Column(db.String(128), nullable=False)
    posts = db.relationship('Post', backref='author', lazy=True)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    slug = db.Column(db.String(200), unique=True, nullable=False)
    body = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)

Routing and Views

All public routes live in app/routes.py. Below is a concise example that covers the home page, single post view, and a simple pagination system.

from flask import Blueprint, render_template, abort, request
from .models import Post

main = Blueprint('main', __name__)

@main.route('/')
def index():
    page = request.args.get('page', 1, type=int)
    pagination = Post.query.order_by(Post.created_at.desc()).paginate(
        page, per_page=5, error_out=False
    )
    posts = pagination.items
    return render_template('index.html', posts=posts, pagination=pagination)

@main.route('/post/<slug>')
def post_detail(slug):
    post = Post.query.filter_by(slug=slug).first_or_404()
    return render_template('post.html', post=post)

Templates: SEO‑Friendly Markup

Use Jinja2 to inject dynamic content while keeping the HTML clean for search engines. A minimal base.html might look like this:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ title|default('My Flask Blog') }}</title>
    <meta name="description" content="{{ meta_description|default('A Python Flask blog tutorial') }}">
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <header>
        <h1><a href="{{ url_for('main.index') }}">My Flask Blog</a></h1>
    </header>
    <main>
        {% block content %}{% endblock %}
    </main>
    <footer>
        <p>© {{ current_year }} My Flask Blog. All rights reserved.</p>
    </footer>
</body>
</html>

In index.html you can loop through posts and add structured data for rich snippets:

{% extends "base.html" %}
{% block content %}
    <h2>Latest Posts</h2>
    {% for post in posts %}
        <article itemscope itemtype="https://schema.org/BlogPosting">
            <h3 itemprop="headline">
                <a href="{{ url_for('main.post_detail', slug=post.slug) }}">{{ post.title }}</a>
            </h3>
            <time datetime="{{ post.created_at.isoformat() }}" itemprop="datePublished">
                {{ post.created_at.strftime('%B %d, %Y') }}
            </time>
            <p itemprop="description">{{ post.body|truncate(150) }}</p>
        </article>
    {% else %}
        <p>No posts yet. Stay tuned!</p>
    {% endfor %}

    {% if pagination.pages > 1 %}
        <nav class="pagination">
            {% if pagination.has_prev %}
                <a href="{{ url_for('main.index', page=pagination.prev_num) }}">Previous</a>
            {% endif %}
            <span>Page {{ pagination.page }} of {{ pagination.pages }}</span>
            {% if pagination.has_next %}
                <a href="{{ url_for('main.index', page=pagination.next_num) }}">Next</a>
            {% endif %}
        </nav>
    {% endif %}
{% endblock %}

Adding Markdown Support

Readers love formatted text, and Markdown is a lightweight way to achieve it. Install Flask-Markdown (already in the requirements) and enable it in the factory. Then, in post.html render the body safely:

{% extends "base.html" %}
{% block content %}
    <article itemscope itemtype="https://schema.org/BlogPosting">
        <h2 itemprop="headline">{{ post.title }}</h2>
        <time datetime="{{ post.created_at.isoformat() }}" itemprop="datePublished">
            {{ post.created_at.strftime('%B %d, %Y') }}
        </time>
        <div itemprop="articleBody">
            {{ post.body|markdown }}
        </div>
    </article>
{% endblock %}

Implementing User Authentication (Optional but Recommended)

Secure your blog’s admin area with Flask‑Login and Werkzeug’s password hashing. A minimal login form in forms.py:

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email

class LoginForm(FlaskForm):
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])
    submit = SubmitField('Log In')

And the corresponding view:

@main.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *