Python Flask Blog Application Tutorial

Written by

in

Looking to turn your Python skills into a real‑world web project? In this step‑by‑step tutorial you’ll learn how to build a fully functional blog application with Flask, the lightweight yet powerful web framework that powers everything from micro‑services to large‑scale sites. By the end of the guide you’ll have a clean, modular Flask blog that supports posting, editing, deleting, and commenting—all while following best practices for project structure, security, and SEO.

Why Choose Flask for a Blog?

Flask is a micro‑framework that gives you just enough tools to get started, while letting you add extensions only when you need them. This makes it an ideal choice for a blog because you can:

  • Keep the codebase lightweight and easy to understand.
  • Customize the database layer (SQLAlchemy, Peewee, or even plain SQLite).
  • Integrate SEO‑friendly features like clean URLs, meta tags, and sitemaps.
  • Scale later with extensions such as Flask‑Login, Flask‑Migrate, and Flask‑Cache.

Project Overview

Our Flask blog will include the following core features:

  1. Home page displaying a list of blog posts.
  2. Individual post pages with full content and comments.
  3. Admin interface for creating, editing, and deleting posts.
  4. User authentication (optional, using Flask‑Login).
  5. SEO enhancements: meta titles, descriptions, and a sitemap.

Prerequisites

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

  • Python 3.9+ (python --version)
  • Virtualenv (python -m pip install virtualenv)
  • Git (optional, for version control)

Step 1: Set Up the Project Structure

A clean folder layout helps you stay organized and makes future maintenance a breeze. Create the directories as shown below:

flask_blog/
├── app/
│   ├── __init__.py
│   ├── models.py
│   ├── routes.py
│   ├── forms.py
│   ├── templates/
│   │   ├── base.html
│   │   ├── index.html
│   │   ├── post.html
│   │   └── admin/
│   │       ├── edit_post.html
│   │       └── new_post.html
│   └── static/
│       ├── css/
│       │   └── style.css
│       └── img/
├── migrations/
├── venv/
├── config.py
└── run.py

What Each File Does

  • run.py – Entry point that launches the Flask development server.
  • config.py – Central place for configuration variables (secret key, database URI, etc.).
  • app/__init__.py – Creates the Flask app, registers extensions, and loads blueprints.
  • app/models.py – Defines database models for Post and Comment.
  • app/routes.py – Contains view functions (URL routes) for the public and admin sections.
  • app/forms.py – WTForms classes for post creation and comment submission.
  • templates/ – Jinja2 HTML templates that render the UI.

Step 2: Initialize the Flask Application

Open app/__init__.py and add the following code:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_wtf import CSRFProtect

db = SQLAlchemy()
migrate = Migrate()
csrf = CSRFProtect()

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

    # Initialize extensions
    db.init_app(app)
    migrate.init_app(app, db)
    csrf.init_app(app)

    # Register blueprints
    from .routes import main_bp, admin_bp
    app.register_blueprint(main_bp)
    app.register_blueprint(admin_bp, url_prefix='/admin')

    return app

And in run.py:

from app import create_app

app = create_app()

if __name__ == '__main__':
    app.run(debug=True)

Step 3: Configure the Application

In config.py define a simple configuration class. For production you would use environment variables, but for this tutorial a static secret works fine.

import os

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

Step 4: Create Database Models

Open app/models.py and define the Post and Comment models. Include SEO‑relevant fields like slug and meta_description.

from datetime import datetime
from . import db

class Post(db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(150), nullable=False)
    slug = db.Column(db.String(150), unique=True, nullable=False)
    body = db.Column(db.Text, nullable=False)
    meta_description = db.Column(db.String(160))
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow)
    comments = db.relationship('Comment', backref='post', cascade='all, delete-orphan')

    def __repr__(self):
        return f''

class Comment(db.Model):
    __tablename__ = 'comments'
    id = db.Column(db.Integer, primary_key=True)
    author = db.Column(db.String(80), nullable=False)
    content = db.Column(db.Text, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    post_id = db.Column(db.Integer, db.ForeignKey('posts.id'), nullable=False)

    def __repr__(self):
        return f''

Step 5: Build the Forms with WTForms

WTForms makes handling user input safe and convenient. Add the following to 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=150)])
    slug = StringField('Slug', validators=[DataRequired(), Length(max=150)])
    meta_description = StringField('Meta Description', validators=[Length(max=160)])
    body = TextAreaField('Content', validators=[DataRequired()])
    submit = SubmitField('Publish')

class CommentForm(FlaskForm):
    author = StringField('Name', validators=[DataRequired(), Length(max=80)])
    content = TextAreaField('Comment', validators=[DataRequired()])
    submit = SubmitField('Add Comment')

Step 6: Define Routes and Views

We’ll split routes into two blueprints: one for public pages (main_bp) and one for admin actions (admin_bp). Create app/routes.py with the following content:

from flask import Blueprint, render_template, redirect, url_for, flash, request, abort
from . import db
from .models import Post, Comment
from .forms import PostForm, CommentForm
from slugify import slugify

main_bp = Blueprint('main', __name__)
admin_bp = Blueprint('admin', __name__)

# ---------- Public Routes ----------
@main_bp.route('/')
def index():
posts = Post.query.order_by(Post.created_at.desc()).all()
return render_template('index.html', posts=posts)

@main_bp.route('/post/', methods=['GET', 'POST'])
def post_detail(slug):
post = Post.query.filter_by(slug=slug).first_or_404()
form = CommentForm()
if form.validate_on_submit():
comment = Comment(
author=form.author.data,
content=form.content.data,
post=post
)
db.session.add(comment)
db.session.commit()
flash('Your comment has been added.', 'success')
return redirect(url_for('main.post_detail', slug=slug))
return render_template('post.html', post=post, form=form)

# ---------- Admin Routes ----------
@admin_bp.route('/')
def admin_dashboard():
posts = Post.query.order_by(Post.created_at.desc()).all()
return render_template('admin/dashboard.html', posts=posts)

@admin_bp.route('/new', methods=['GET', 'POST'])
def new_post():
form = PostForm()
if form.validate_on_submit():
post = Post(
title=form.title.data,
slug=slugify(form.slug.data),
meta_description=form.meta_description.data,
body=form.body.data
)
db.session.add(post)
db.session.commit()
flash('Post created successfully!', 'success')
return redirect(url_for('admin.admin_dashboard'))
return render_template('admin/new_post.html', form=form)

@admin_bp.route('/edit/', 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.slug = slugify(form.slug.data)
post.meta_description = form.meta_description.data
post.body = form.body.data
db.session.commit()
flash('Post updated successfully.', 'success')
return redirect(url_for('admin.admin_dashboard'))
return render

Comments

Leave a Reply

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