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:
- Home page displaying a list of blog posts.
- Individual post pages with full content and comments.
- Admin interface for creating, editing, and deleting posts.
- User authentication (optional, using Flask‑Login).
- 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 forPostandComment.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
Leave a Reply