Python Flask Blog Application Tutorial

Written by

in

Looking to build a modern, 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, SEO‑friendly blog that you can deploy in minutes. Whether you’re a beginner eager to learn Flask or a seasoned developer needing a quick starter project, the code snippets, clear explanations, and best‑practice tips below will have your blog up and running without the usual hassle.

Why Choose Flask for a Blog?

Flask is a micro‑framework that gives you just enough structure to build powerful web apps while keeping the codebase clean and maintainable. Its lightweight nature makes it ideal for a blog because you can:

  • Add only the extensions you need (SQLAlchemy, Flask‑Login, etc.).
  • Customize the routing and templating exactly how you want.
  • Keep the project simple enough for easy learning and rapid prototyping.

Prerequisites

Before diving in, make sure you have the following installed on your machine:

  • Python 3.9+ – the tutorial uses type hints and f‑strings.
  • pip – Python’s package manager.
  • A code editor (VS Code, PyCharm, or your favorite IDE).
  • Basic knowledge of HTML and CSS (optional but helpful).

Project Setup

1. Create a virtual environment

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

2. Install required packages

pip install Flask Flask-SQLAlchemy Flask-Migrate Flask-WTF
pip install python-dotenv   # For environment variables

3. Directory layout

Organize your files as shown below. This structure keeps your routes, models, and templates separate, which improves readability and SEO maintenance.

flask_blog/
│
├─ app/
│   ├─ __init__.py
│   ├─ models.py
│   ├─ routes.py
│   ├─ forms.py
│   └─ templates/
│       ├─ base.html
│       ├─ index.html
│       ├─ post.html
│       └─ edit_post.html
│
├─ migrations/          # Auto‑generated by Flask‑Migrate
├─ .env                 # Secret keys & DB URI
├─ config.py
└─ run.py

Configuring Flask and the Database

config.py

import os
from dotenv import load_dotenv

load_dotenv()  # Loads variables from .env

class Config:
    SECRET_KEY = os.getenv('SECRET_KEY', 'dev_key')
    SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', 'sqlite:///blog.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False

Initializing the app (app/__init__.py)

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from config import Config

db = SQLAlchemy()
migrate = Migrate()

def create_app():
    app = Flask(__name__)
    app.config.from_object(Config)

    db.init_app(app)
    migrate.init_app(app, db)

    # Register blueprints or routes
    from .routes import main
    app.register_blueprint(main)

    return app

Defining the Blog Model

The model represents a blog post with fields that are both SEO‑friendly (title, slug, timestamps) and functional (content, author).

# app/models.py
from . import db
from datetime import datetime
from slugify import slugify  # pip install python-slugify

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(140), nullable=False)
    slug = db.Column(db.String(150), unique=True, nullable=False)
    content = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # Auto‑generate a URL‑safe slug from the title
        if not self.slug:
            self.slug = slugify(self.title)

    def __repr__(self):
        return f''

Creating Forms with Flask‑WTF

Using WTForms simplifies validation and protects against CSRF attacks.

# app/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length

class PostForm(FlaskForm):
    title = StringField('Title', validators=[DataRequired(), Length(max=140)])
    content = TextAreaField('Content', validators=[DataRequired()])
    submit = SubmitField('Publish')

Routing and View Functions

All blog routes live in app/routes.py. The blueprint pattern keeps the main application clean.

# app/routes.py
from flask import Blueprint, render_template, redirect, url_for, flash, request, abort
from . import db
from .models import Post
from .forms import PostForm

main = Blueprint('main', __name__)

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

@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)

@main.route('/create', methods=['GET', 'POST'])
def create_post():
    form = PostForm()
    if form.validate_on_submit():
        new_post = Post(title=form.title.data, content=form.content.data)
        db.session.add(new_post)
        db.session.commit()
        flash('Post created successfully!', 'success')
        return redirect(url_for('main.post_detail', slug=new_post.slug))
    return render_template('edit_post.html', form=form, legend='New Post')

@main.route('/edit/<int:post_id>', methods=['GET', 'POST'])
def edit_post(post_id):
    post = Post.query.get_or_404(post_id)
    form = PostForm(obj=post)
    if form.validate_on_submit():
        post.title = form.title.data
        post.content = form.content.data
        db.session.commit()
        flash('Post updated!', 'success')
        return redirect(url_for('main.post_detail', slug=post.slug))
    return render_template('edit_post.html', form=form, legend='Edit Post')

@main.route('/delete/<int:post_id>', methods=['POST'])
def delete_post(post_id):
    post = Post.query.get_or_404(post_id)
    db.session.delete(post)
    db.session.commit()
    flash('Post deleted.', 'info')
    return redirect(url_for('main.index'))

Template Design for SEO

Base layout (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>
    <meta name="description" content="A simple, SEO‑optimized blog built with Python Flask.">
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <nav>
        <a href="{{ url_for('main.index') }}">Home</a> |
        <a href="{{ url_for('main.create_post') }}">New Post</a>
    </nav>
    <main>
        {% with messages = get_flashed_messages(with_categories=true) %}
            {% if messages %}
                <ul class="flashes">
                {% for category, message in messages %}
                    <li class="{{ category }}">{{ message }}</li>
                {% endfor %}
                </ul>
            {% endif %}
        {% endwith %}
        {% block content %}{% endblock %}
    </main>
</body>
</html>

Index page (templates/index.html)

{% extends 'base.html' %}
{% block title %}Home – Flask Blog{% endblock %}
{% block content %}
<h2>Latest Posts</h2>
{% for post in posts.items %}
<article>
<h3><a href="{{ url_for('main.post_detail', slug=post.slug) }}">{{ post.title }}</a></h3>
<p>{{ post.content[:200] }}...</p>
<small>Posted on {{ post.created_at.strftime('%b %d, %Y') }}</small>
</article>
{% else %}
<p>No posts yet. <a href="{{ url_for('main.create_post') }}">Create one</a>!</p>
{% endfor %}

<nav class="pagination">
{% if posts.has_prev %}
<a href="{{ url_for('main.index', page=posts.prev_num) }}">Previous</a>
{% endif %}
{% if posts.has_next %}
<a href="{{ url_for('main.index', page=posts.next_num) }}"

Comments

Leave a Reply

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