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) }}"
Leave a Reply