Backend Development 26 min read

Flask‑Based Online Blog Platform Tutorial with User Authentication, Markdown Editing, and CRUD Operations

This article provides a step‑by‑step guide to building an online blog platform using Flask and MySQL, covering environment setup, database schema design, user registration and login, password encryption, CRUD operations for posts, Markdown editing, image upload, comment management, and error handling with full code examples.

Python Programming Learning Circle
Python Programming Learning Circle
Python Programming Learning Circle
Flask‑Based Online Blog Platform Tutorial with User Authentication, Markdown Editing, and CRUD Operations

This document describes a complete Flask web application that implements an online blogging platform. It outlines the development environment (PyCharm, Python 3.7, MySQL 5.5) and the technology stack (Flask for the backend, Layui and a Markdown editor for the frontend).

Project Description

The platform supports user registration, login, password changes, creating, editing, and deleting blog posts, viewing all posts, commenting, and managing personal blogs and comments. The database consists of three tables: users, blogs, and comments, with one‑to‑many relationships.

Database Design

The following SQLAlchemy models define the database schema:

<code>class User(db.Model):
    __tablename__ = 'tb_user'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    username = db.Column(db.String(64), unique=True)
    password = db.Column(db.String(256), nullable=True)
    name = db.Column(db.String(64))

    def password_hash(self, password):
        self.password = generate_password_hash(password)

class Blog(db.Model):
    __tablename__ = 'blog'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    title = db.Column(db.String(128))
    text = db.Column(db.TEXT)
    create_time = db.Column(db.String(64))
    user_id = db.Column(db.Integer, db.ForeignKey('tb_user.id'))
    user = db.relationship('User', backref='user')

class Comment(db.Model):
    __tablename__ = 'comment'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    text = db.Column(db.String(256))
    create_time = db.Column(db.String(64))
    blog_id = db.Column(db.Integer, db.ForeignKey('blog.id'))
    user_id = db.Column(db.Integer, db.ForeignKey('tb_user.id'))
    blog = db.relationship('Blog', backref='blog')
    user = db.relationship('User', backref='use')
</code>

Template Structure

The UI uses Jinja2 templates with a base layout (base.html) that defines the navigation bar and includes Layui CSS/JS. Individual pages extend this base template.

<code>&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;{% block title %}{% endblock %}&lt;/title&gt;
    &lt;link rel="stylesheet" href="/static/layui/css/layui.css"&gt;
    &lt;link rel="stylesheet" href="/static/css/base.css"&gt;
    &lt;script src="/static/js/jquery.js"&gt;&lt;/script&gt;
    &lt;script src="/static/layui/layui.js"&gt;&lt;/script&gt;
    {% block css %}{% endblock %}
&lt;/head&gt;
&lt;body&gt;
    &lt;ul class="layui-nav" lay-filter=""&gt;
        &lt;li class="layui-nav-item"&gt;&lt;a href="/"&gt;在线博客平台&lt;/a&gt;&lt;/li&gt;
        {% if username %}
            &lt;li class="layui-nav-item"&gt;&lt;a href="/updatePwd"&gt;修改密码&lt;/a&gt;&lt;/li&gt;
        {% endif %}
        &lt;li class="layui-nav-item"&gt;&lt;a href="/blog/blogAll"&gt;博客&lt;/a&gt;&lt;/li&gt;
        &lt;li class="layui-nav-item"&gt;&lt;a href="/about"&gt;关于&lt;/a&gt;&lt;/li&gt;
        {% if username %}
            &lt;li class="layui-nav-item" style="float:right; margin-right:30px;"&gt;
                &lt;a href="javascript:;"&gt;{{ name }}&lt;/a&gt;
                &lt;dl class="layui-nav-child"&gt;
                    &lt;dd&gt;&lt;a href="/blog/myBlog"&gt;我的博客&lt;/a&gt;&lt;/dd&gt;
                    &lt;dd&gt;&lt;a href="/blog/myComment"&gt;我的评论&lt;/a&gt;&lt;/dd&gt;
                    &lt;dd&gt;&lt;a href="/logout"&gt;注销&lt;/a&gt;&lt;/dd&gt;
                &lt;/dl&gt;
            &lt;/li&gt;
            &lt;li class="layui-nav-item" style="float:right"&gt;&lt;a href="/blog/writeBlog"&gt;写博客&lt;/a&gt;&lt;/li&gt;
        {% else %}
            &lt;li class="layui-nav-item" style="float:right"&gt;&lt;a href="/register"&gt;注册&lt;/a&gt;&lt;/li&gt;
            &lt;li class="layui-nav-item" style="float:right"&gt;&lt;a href="/login"&gt;登录&lt;/a&gt;&lt;/li&gt;
        {% endif %}
    &lt;/ul&gt;
    &lt;div class="content"&gt;{% block content %}{% endblock %}&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code>

Authentication

Login and registration views handle GET/POST requests, verify passwords with check_password_hash , and store the username in the session. Passwords are stored securely using Werkzeug’s hashing functions.

<code># 登录请求
@index.route('/login', methods=['POST', 'GET'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        user = User.query.filter(User.username == username).first()
        if (user is not None) and (check_password_hash(user.password, password)):
            session['username'] = user.username
            session.permanent = True
            return redirect(url_for('index.hello'))
        else:
            flash('账号或密码错误')
            return render_template('login.html')
</code>

Password Change

The password‑change view validates the original password, ensures the new passwords match, and updates the stored hash.

<code># 修改密码
@index.route('/updatePwd', methods=['POST', 'GET'])
@login_limit
def update():
    if request.method == 'GET':
        return render_template('updatePwd.html')
    if request.method == 'POST':
        lodPwd = request.form.get('lodPwd')
        newPwd1 = request.form.get('newPwd1')
        newPwd2 = request.form.get('newPwd2')
        username = session.get('username')
        user = User.query.filter(User.username == username).first()
        if check_password_hash(user.password, lodPwd):
            if newPwd1 != newPwd2:
                flash('两次新密码不一致!')
                return render_template('updatePwd.html')
            else:
                user.password_hash(newPwd2)
                db.session.commit()
                flash('修改成功!')
                return render_template('updatePwd.html')
        else:
            flash('原密码错误!')
            return render_template('updatePwd.html')
</code>

Blog CRUD

Creating a blog post captures the title, content (Markdown), author, and timestamp, then saves it to the database.

<code># 写博客页面
@blog.route('/writeBlog', methods=['POST', 'GET'])
@login_limit
def writeblog():
    if request.method == 'GET':
        return render_template('writeBlog.html')
    if request.method == 'POST':
        title = request.form.get('title')
        text = request.form.get('text')
        username = session.get('username')
        create_time = time.strftime('%Y-%m-%d %H:%M:%S')
        user = User.query.filter(User.username == username).first()
        blog = Blog(title=title, text=text, create_time=create_time, user_id=user.id)
        db.session.add(blog)
        db.session.commit()
        blog = Blog.query.filter(Blog.create_time == create_time).first()
        return render_template('blogSuccess.html', title=title, id=blog.id)
</code>

Listing, viewing, updating, and deleting blogs are implemented with similar route functions, using order_by to show the newest posts first.

<code># 展示全部博客
@blog.route('/blogAll')
def blogAll():
    blogList = Blog.query.order_by(Blog.create_time.desc()).all()
    return render_template('blogAll.html', blogList=blogList)
</code>

Markdown Editing and Image Upload

The write‑blog page integrates the editormd Markdown editor. Image uploads are handled via a dedicated POST endpoint that saves the file with a UUID filename and returns a JSON response.

<code># 上传图片
@blog.route('/imgUpload', methods=['POST'])
@login_limit
def imgUpload():
    try:
        file = request.files.get('editormd-image-file')
        fname = secure_filename(file.filename)
        ext = fname.rsplit('.')[-1]
        fileName = str(uuid.uuid4()) + '.' + ext
        filePath = os.path.join('static/uploadImg/', fileName)
        file.save(filePath)
        return {'success': 1, 'message': '上传成功!', 'url': '/' + filePath}
    except Exception:
        return {'success': 0, 'message': '上传失败'}
</code>

Comment System

Authenticated users can post comments on blog detail pages. Comments are stored with references to both the blog and the user.

<code># 评论
@blog.route('/comment', methods=['POST'])
@login_limit
def comment():
    text = request.values.get('text')
    blogId = request.values.get('blogId')
    username = session.get('username')
    create_time = time.strftime('%Y-%m-%d %H:%M:%S')
    user = User.query.filter(User.username == username).first()
    comment = Comment(text=text, create_time=create_time, blog_id=blogId, user_id=user.id)
    db.session.add(comment)
    db.session.commit()
    return {'success': True, 'message': '评论成功!'}
</code>

User Personal Pages

Pages for "My Blog" and "My Comment" list the current user's posts and comments, respectively, with options to edit or delete them.

<code># 查看个人博客
@blog.route('/myBlog')
@login_limit
def myBlog():
    username = session.get('username')
    user = User.query.filter(User.username == username).first()
    blogList = Blog.query.filter(Blog.user_id == user.id).order_by(Blog.create_time.desc()).all()
    return render_template('myBlog.html', blogList=blogList)
</code>

Error Handling

Custom 404 and 500 error handlers render a friendly error page that displays a background image and a link back to the homepage.

<code># 404页面
@app.errorhandler(404)
def page_not_found(e):
    return render_template('404.html'), 404

# 500页面
@app.errorhandler(500)
def internal_server_error(e):
    return render_template('404.html'), 500
</code>

Overall, the tutorial demonstrates a full‑stack Python web application with user authentication, secure password handling, Markdown content creation, image uploads, comment management, and graceful error handling, providing a solid foundation for further feature expansion.

PythonMySQLAuthenticationWeb DevelopmentFlaskCRUDMarkdown
Python Programming Learning Circle
Written by

Python Programming Learning Circle

A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.