How to build a Blog website using Flask in python

A beginner-friendly tutorial on building a blog site with responsive web design, user authentication, and DB CRUB operation.

Introduction

Flask is a web framework, it’s a Python module that lets you develop web applications quickly. Flask is based on the Werkzeg WSGI toolkit and the Jinja2 template engine. It is designed to keep the core of the application simple and scalable.
Flask uses the Jinja template engine to dynamically build HTML pages using familiar Python concepts such as variables, loops, lists, etc. You’ll use these templates as part of this project.
In this tutorial, you’ll build a small web blog using Flask and SQLite in Python 3. Users of the application can view all the posts in your database and click on the title of a post to view its contents with the ability to add a new post to the database and edit or delete an existing post. Users will also be able to comment under a post and view other users' comments. Also, Users will be able to upload their profile pictures and reset their passwords.

Prerequisites

The project was implemented using HTML, CSS, and JavaScript for the front end, and Python for the back end. We also used the Flask framework and the flask library to simplify the development process.

Before you start following this guide, you will need:

  • A local Python 3 programming environment with an understanding of Python 3 concepts such as data types, functions, for loops, and so on.

  • Visual Studio Code (VS Code) or any other IDE.

  • Bash Terminal Shell: this could be Git Bash or other terminals like Command Prompt.

Initial Preparation

Create a Directory for the Project

It is a good habit to have a root directory for your project and properly organized on the system. For this project, we will create a folder for it on the computer and name it flasky_blog then move into the folder to start working on it. This can be done from the computer's file explorer or terminal.
Let's create it now:

$ mkdir flasky_blog
$ cd flasky_blog

Set up and activate the virtual environment

A virtual environment is a tool that helps to keep dependencies required by different projects separate by creating isolated python virtual environments for them.
A virtual Environment should be used whenever you work on any Python-based project. It is generally good to have one new virtual environment for every Python-based project you work on. So the dependencies of every project are isolated from the system and each other.

Warning: Do not touch or edit any files in the virtual environment folder!

For simplicity, we will use the syntax for Windows in the next two code blocks. Mac/Linux users, please check out the applicable syntax from Flask Docs instead.

We start by creating a virtual environment named env :

> py -m venv env

The next step is to activate the virtual environment and we are going to do that with this command:

> env\Scripts\activate

Once the virtual environment is activated, the name of your virtual environment will appear on the left side of the terminal. Here is an example of what an activated virtual environment looks like:

> (env) C:\Users\User\flasky_blog>

Note: Once you are done with the work, you can deactivate the virtual environment by the following command:

> deactivate

Now you will be back to the system’s default Python installation. But we are still building the blog website for now, so there is no reason to deactivate the virtual environment for now.

How to install packages e.g Flask

When working on a project, make sure your virtual environment is active before installing any packages to make sure it is saved in the project folder.
Pip is the package installer for Python. It is a very useful module that comes bundled with Python, so we will come across it often when installing Python libraries.

After activating the environment, we will install Flask using this pip command:

> pip install Flask

The ‘pip’ simply means a package manager for Python packages. The ‘install’ means the installation of a particular package into the root workplace, and lastly ‘Flask’ is the micro web framework you are trying to install.

Backend Preliminaries

Now that you have your programming environment set up, you’ll start using Flask.

Next up, you’ll make a small web application inside a Python file and run it to start the server, which will display some information on the browser.

Creating a Flask Application

First, we will need to create a .flaskenv file in your flask_blog directory.
This hidden file enables us to launch the blog with a flask run command in the terminal, and will have the following lines of code in plain text:

FLASK_ENV = development
FLASK_DEBUG = 1
FLASK_APP = app.py

With these statements, we have told the computer that:

  • With these statements, we have told the computer that:

  • We are willing to debug the Flask app as it runs (1 is "True" in Boolean)

  • We want Flask to recognize app.py as the main Flask app file for this project.

This brings us to app.py, eventually the heaviest file in this project. Note that it is better to split the code we will have in app.py into different Python files which carry out specific functions, but we are keeping things simple here.

In your flasky_blog directory, create a file and name it app.py. After doing this, open the file in your favorite editor but for this course, we will use VS Code, then write this code within:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, World!'

In the preceding code block, you first import the Flask object from the flask package. You then use it to create your Flask application instance with the name app. You pass the special variable __name__ that holds the name of the current Python module. It’s used to tell the instance where it’s located—you need this because Flask sets up some paths behind the scenes.
Once you create the app instance, you use it to handle incoming web requests and send responses to the user. @app.route is a decorator that turns a regular Python function into a Flask view function, which converts the function’s return value into an HTTP response to be displayed by an HTTP client, such as a web browser. You pass the value '/' to @app.route() to signify that this function will respond to web requests for the URL /, which is the main URL.
The hello() view function returns the string 'Hello, World!' as a response. Save the file.
Lastly, run the application using the flask run command:

> (env) C:\Users\User\flasky_blog> flask run

Once the application is running the output will be something like this:

Output
 * Serving Flask app "app.py" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 142-795-068

The preceding output has several pieces of information, such as:

  • The name of the application you’re running.

  • The environment in which the application is being run.

  • Debug mode: on signifies that the Flask debugger is running. This is useful when developing because it gives us detailed error messages when things go wrong, which makes troubleshooting easier.

  • The application is running locally on the URL http://127.0.0.1:5000/, 127.0.0.1 is the IP that represents your machine’s localhost and :5000 is the port number.

  • Note: The debugger PIN will be different from yours when you run the server. So do not panic whenever it is different.

Open a browser and type in the URL http://127.0.0.1:5000/, you will receive the string Hello, World! as a response, this confirms that your application is successfully running.

Using HTML Templates with Flask

Currently, your application only displays a simple message without any HTML.

Web applications mainly use HTML to display information for the visitor, so you’ll now practice some HTML and CSS, and learn how to use HTML templates with Jinja., which can be displayed on the web browser.

Flask provides a render_template() helper function that allows the use of the Jinja template engine. This will make managing HTML much easier by writing your HTML code in .html files as well as using logic in your HTML code. You’ll use these HTML files, (templates) to build all of your application pages, such as the main page where you’ll display the current blog posts, the page of the blog post, the page where the user can add a new post, and so on.

Creating the templates.

We start this step by creating a templates folder within the project's main folder or root directory. This folder is a standard in Flask development, as it is from here that all HTML files used in the project will be accessed by the Flask app.

In this file, you’ll import the Flask object to create a Flask application instance as you previously did. You’ll also import the render_template() helper function that lets you render HTML template files that exist in the templates folder you have created. The file will have a single view function that will be responsible for handling requests to the main / route. Add the following content:

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

The index() view function returns the result of calling render_template() with index.html as an argument, this tells render_template() to look for a file called index.html in the templates folder. Both the folder and the file do not yet exist, you will get an error if you were to run the application at this point. You’ll run it nonetheless so you’re familiar with this commonly encountered exception. You’ll then fix it by creating the needed folder and file.
Save the file.
Run the application:

> (env) C:\Users\User\flasky_blog> flask run

Opening the URL http://127.0.0.1:5000/ in your browser will result in the debugger page informing you that the index.html template was not found. The main line in the code that was responsible for this error will be highlighted. In this case, it is the line return render_template('index.html').

To fix this error, in the templates folder you created before. Then inside it, create and open a file called index.html for editing:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>FlaskBlog</title>
</head>
<body>
   <h1>Welcome to FlaskBlog</h1>
</body>
</html>

Save the file and use your browser to navigate to http://127.0.0.1:5000/ again, or refresh the page. This time the browser should display the text Welcome to FlaskBlog in an <h1> tag.

Style with CSS in a Flask App

Just like the templates folder in the previous step, the static folder is a standard for front-end development with Flask. A Flask project's static folder contains all assets used by the project's templates, including CSS files, JavaScript files, and images. Within the static folder, we will create a css folder, which will then have our main.css file. For this beginner-friendly project, all styling will be done within main.css.

Then open a main.css file inside the css directory for editing.
Add the following CSS rule to your style.css file:

h1 {
    border: 2px #eee solid;
    color: brown;
    text-align: center;
    padding: 10px;
}

The CSS code will add a border, change the color to brown, center the text, and add a little padding to <h1> tags. Save the file.

Next, open the index.html template file for editing. You’ll add a link to the style.css file inside the <head> section of the index.html template file:

. . .
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="{{ url_for('static', filename= 'style.css') }}">
    <title>FlaskBlog</title>
</head>
. . .

Here you use the url_for() helper function to generate the appropriate location of the file. The first argument specifies that you’re linking to a static file and the second argument is the path of the file inside the static directory.

Check if your server is still running, if so refresh the browser and if it is not running, restart the server using flask run in your terminal. Upon refreshing the index page of your application, you will notice that the text Welcome to FlaskBlog is now brown, centered, and enclosed inside a border.

You can use the CSS language to style the application and make it more appealing using your own design. However, if you’re not a web designer, or if you aren’t familiar with CSS, then you can use the Bootstrap toolkit, which provides easy-to-use components for styling your application. In this project, we’ll use Bootstrap.

You might have guessed that making another HTML template would mean repeating most of the HTML code you already wrote in the index.html template. You can avoid unnecessary code repetition with the help of a base template file, which all of your HTML files will inherit from. See Template Inheritance in Jinja for more information.
To make a base template. First, create a file called base.html inside your templates directory and open it for editing:

<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

    <title>{% block title %} {% endblock %}</title>
  </head>
  <body>
    <nav class="navbar navbar-expand-md navbar-light bg-light">
        <a class="navbar-brand" href="{{ url_for('index')}}">FlaskBlog</a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav">
            <li class="nav-item active">
                <a class="nav-link" href="#">About</a>
            </li>
            </ul>
        </div>
    </nav>
    <div class="container">
        {% block content %} {% endblock %}
    </div>

    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
  </body>
</html>

Most of the code in the preceding block is standard HTML and code required for Bootstrap. The <meta> tags provide information for the web browser, the <link> tag links the Bootstrap CSS files and the <script> tags are links to JavaScript code that allows some additional Bootstrap features, check out the Bootstrap documentation for more.
However, the following highlighted parts are specific to the Jinja template engine:

  • {% block title %} {% endblock %}: A block that serves as a placeholder for a title, you’ll later use it in other templates to give a custom title for each page in your application without rewriting the entire <head> section each time.

  • {{ url_for('index')}}: A function call that will return the URL for the index() view function. This is different from the past url_for() call you used to link a static CSS file because it only takes one argument, which is the view function’s name, and links to the route associated with the function instead of a static file.

  • {% block content %} {% endblock %}: Another block that will be replaced by content depending on the child template (templates that inherit from base.html) that will override it.

Now that you have a base template, you can take advantage of it using inheritance. Open the index.html file. Then replace its contents with the following:

{% extends 'base.html' %}

{% block content %}
    <h1>{% block title %} Welcome to FlaskBlog {% endblock %}</h1>
{% endblock %}

In this new version of the index.html template, you use the {% extends %} tag to inherit from the base.html template. You then extend it by replacing the content block in the base template with what is inside the content block in the preceding code block.

This content block contains an <h1> tag with the text Welcome to FlaskBlog inside a title block, which in turn replaces the original title block in the base.html template with the text Welcome to FlaskBlog. This way, you can avoid repeating the same text twice, as it works both as a title for the page and a heading that appears below the navigation bar inherited from the base template.

Template inheritance also gives you the ability to reuse the HTML code you have in other templates (base.html in this case) without having to repeat it each time it is needed.

Save the file and refresh the index page on your browser. You’ll see your page with a navigation bar and a styled title.

You’ve used HTML templates and static files in Flask. You also used Bootstrap to start refining the look of your page and a base template to avoid code repetition. In the next step, you’ll set up a database that will store your application data.

Setting up the Database

In this step, you’ll set up a database to store data, that is, the blog posts for your application. You’ll also populate the database with a few example entries.
You’ll use an SQLite database file to store your data because the sqlite3 module, which we will use to interact with the database, is readily available in the standard Python library. You can use Flask-SQLAlchemy, a Flask extension that simplifies the use of SQLAlchemy, a popular Python library for interacting with databases.

Let's get started.
First, we need to install the package and we will do so in the terminal. Make sure your virtual environment is still turned on before installing any package.

> (env) C:\Users\User\flasky_blog> pip install Flask-SQLAlchemy

Now that we have installed the package, let's head over to our app.py file and import it.

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os

app = Flask(__name__)

Let's configure our application now. There are different settings you might want to change depending on the application environment like toggling the debug mode, setting the secret key (which should be unique and a secret), creating the path to the database, and other environment-specific things.
You can generate a secret key that is protected from hackers by following the steps in the snippet below.

> (env) C:\Users\User\flasky_blog>python
Python 3.10.0 (tags/v3.10.0:b494f59) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import secrets
>>> secrets.token_hex(16)
'9fe0dca16218e96c1ef25bc966cbb0b3ed7606ac'
>>> exit()

Note: The secret key that will be generated will be different from yours. So copy and save it in order to avoid losing it.
Now, we will construct a path for our SQLite database file. We will use some functions from the os module to store the path of the base directory in a variable named base_dir. We will then configure SQLAlchemy's URI and secret key, and turn off modification tracking to use less memory.

base_dir = os.path.dirname(os.path.realpath(__file__))

app = Flask(__name__)

app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///' + os.path.join(base_dir, 'database.db')
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SECRET_KEY"] = '9fe0dca16218e96c1ef25bc966cbb0b3ed7606ac'

We now have our database configured. Time to prepare the tables within. We will store our project data with four database models: User, Posts, Comments, and Likes.

Tables are represented by a model which is a Python class that inherits from a base class Flask-SQLAlchemy provides through the db instance created above. A Foreign key is a key that links a table with another, using that table's primary key. You need to supply a primary key for each model, then you need to define a Foreign Key which refers to the primary key of the other model. You can now define a relationship with a backref that allows direct access to the related model.

User Model

Our User model creates a table of users using UserMixin, links each user to their articles in the table, and then returns the username as its representation.

To have access to UserMixin, we will first install Flask-Login in the terminal:

> (env) C:\Users\User\flasky_blog> pip install Flask-Login

Let's define the fields of the model using class variables.

class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    firstname = db.Column(db.String(20), unique=True, nullable=False)
    lastname = db.Column(db.String(20), unique=True, nullable=False)
    username = db.Column(db.String(20), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    image_file = db.Column(db.String(20), nullable=False, default='default.jpeg')
    password = db.Column(db.String(60), nullable=False)
    posts = db.relationship('Post', backref='author', lazy=True)
    comments = db.relationship('Comments', backref='user', lazy=True)
    likes = db.relationship('Likes', backref='user', lazy=True)

    def __repr__(self):
        return f"User <{self.username}>"

Here, you create a User model, which inherits from the db.Model class. This represents the student table. You use the db.Column class to define columns for your table. The first argument represents the column type, and additional arguments represent the column configuration.

Post Model

The Post model creates a table of articles, links each article to its author, and then returns the article's title as its representation.
To be able to show the day each entry was posted, we will need to import from the datetime module that comes with Python:

> (env) C:\Users\User\flasky_blog>  from datetime import datetime

Let's define the fields of the model using class variables.

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    content = db.Column(db.Text, nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    comments = db.relationship('Comments', backref='post', lazy=True)
    likes = db.relationship('Likes', backref='post', lazy=True)
    views = db.Column(db.Integer, default=0)

    def __repr__(self):
        return f"Post('{self.title}', '{self.date_posted}')"

Comments Model

The Comments model creates a table of messages from site visitors and returns the message title as its representation.

class Comments(db.Model):
    __tablename__ = 'comments'
    id = db.Column(db.Integer, primary_key=True)
    text = db.Column(db.String(200), nullable=False)
    date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    author = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
    name = db.Column(db.String(200), unique=False, nullable=False)
    post_id = db.Column(db.Integer, db.ForeignKey('post.id', ondelete='CASCADE'), nullable=False)
    email = db.Column(db.String(200), unique=False, nullable=False)

    def __repr__(self):
        return f"Comment('{self.author}', '{self.text}')"

Note: The special __repr__ function allows you to give each object a string representation to recognize it for debugging purposes. In this case, you use the comments author and its text.

Likes Model

class Likes(db.Model):
    __tablename__='likes'
    id = db.Column(db.Integer, primary_key=True)
    date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    author = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
    post_id = db.Column(db.Integer, db.ForeignKey('post.id', ondelete='CASCADE'), nullable=False)

Contact Model

class Contact(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(120), nullable=False)
    message = db.Column(db.Text, nullable=False)

    def __repr__(self):
        return f"Contact('{self.email}', '{self.message}')"

The app.py file will now look as follows:

# Imports: load the source packages with `pip install -r requirements.txt`
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from datetime import datetime
import os

# Configurations: app, base directory and db
app = Flask(__name__)

base_dir = os.path.dirname(os.path.realpath(__file__))

app.config["SQLALCHEMY_DATABASE_URI"] = 'sqlite:///' + \
    os.path.join(base_dir, 'database.db')
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SECRET_KEY"] = 'createacomplexsecretkeyhere'

db = SQLAlchemy(app)
db.init_app(app)

#Models
class User(db.Model, UserMixin):
    __tablename__='user'
    id = db.Column(db.Integer, primary_key=True)
    firstname = db.Column(db.String(20), unique=True, nullable=False)
    lastname = db.Column(db.String(20), unique=True, nullable=False)
    username = db.Column(db.String(20), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    image_file = db.Column(db.String(20), nullable=False, default='default.jpeg')
    password = db.Column(db.String(60), nullable=False)
    posts = db.relationship('Post', backref='author', lazy=True)
    comments = db.relationship('Comments', backref='user', lazy=True)
    likes = db.relationship('Likes', backref='user', lazy=True)

    def __repr__(self):
        return f"User <{self.username}>"

class Post(db.Model):
    __tablename__='post'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(100), nullable=False)
    date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    content = db.Column(db.Text, nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    comments = db.relationship('Comments', backref='post', lazy=True)
    likes = db.relationship('Likes', backref='post', lazy=True)
    views = db.Column(db.Integer, default=0)

    def __repr__(self):
        return f"Post('{self.title}', '{self.date_posted}')"

class Comments(db.Model):
    __tablename__ = 'comments'
    id = db.Column(db.Integer, primary_key=True)
    text = db.Column(db.String(200), nullable=False)
    date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    author = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
    name = db.Column(db.String(200), unique=False, nullable=False)
    post_id = db.Column(db.Integer, db.ForeignKey('post.id', ondelete='CASCADE'), nullable=False)
    email = db.Column(db.String(200), unique=False, nullable=False)

    def __repr__(self):
        return f"Comment('{self.author}', '{self.text}')"

class Likes(db.Model):
    __tablename__='likes'
    id = db.Column(db.Integer, primary_key=True)
    date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    author = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
    post_id = db.Column(db.Integer, db.ForeignKey('post.id', ondelete='CASCADE'), nullable=False)

class Contact(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    email = db.Column(db.String(120), nullable=False)
    message = db.Column(db.Text, nullable=False)

    def __repr__(self):
        return f"Contact('{self.email}', '{self.message}')"


@app.before_first_request
def create_tables():
    db.create_all()

The syntax below the file ensures that the database file is created the first time a route is requested in this Flask app, only that first time, and only if the database.db file does not already exist. Therefore, When we use flask run in the terminal, a database.db file will appear in the project's root directory if none previously existed. We can then use DB Browser for SQLite to see what is happening within the database, as VS Code does not read .db files without installing the necessary extension.

Using Basic routes

Routing means mapping a URL to a specific Flask function that will handle the logic for that URL. Routing in Flask is done with the route() decorator, and the URL is passed as an argument to the decorator. We will use @app.route() in this tutorial.
Before this, we will need to add a few imports from Flask, leaving us with this statement for the Flask imports line:

from flask import Flask, flash, render_template, url_for, request, redirect

Home page

The index route is always represented with just '/' in routing. Since this is the homepage of a blog, we will make an index() function to display all articles.

@app.route('/')
def index():
     posts = Post.query.order_by(Post.date_posted.desc())
    return render_template('home.html', posts=posts)

Here, we have gotten all of the blog's articles by querying the Post model in our database. See more details about rendering templates explained by Flask Docs.

About Page

This simple function renders the about.html template as a webpage that tells the visitor about the blog site and its creator.

@app.route('/about')
def about():
    return render_template('about.html')

Contact Page

Our contact() function collects feedback from users via a form in the contact.html template, stores it on the "contact" table in the database using the Contact model, and then redirects to the homepage after flashing a success message.

@app.route('/contact', methods=['GET', 'POST'])
def contact():
    form = ContactForm()
    if form.validate_on_submit():
        user = Contact(name=form.name.data, email=form.email.data,
                        message=form.message.data)
        db.session.add(user)
        db.session.commit()
        flash(f"Hey {form.name.data}, Your Message has been Sent!",'success')
        return redirect(url_for('index'))
    return render_template('contact.html', title='Contact', form=form)

Note: The 'GET' method gets data from the backend to display to users, while the 'POST' method allows a user to post data to the backend. 'GET' is the default routing method, so it does not need to be passed as an argument when no other method is used.

Create HTML Pages with Jinja

Jinja lets us make our HTML files dynamic with syntax that resembles Python. Jinja code is written where needed within the HTML code, so it is not stored as a separate script.

Now let's modify our templates folder by adding the following HTML files that will be used for the blog: layout.html, home.html, about.html, contact.html, register.html, login.html, account.html, post.html , user_posts.html, reset_token.html, reset_request.html and create_post.html.

Let's start by editing the base.html which has been renamed to layout.html

layout.html

A base template contains HTML components that are typically shared between all other templates, such as the application’s title, navigation bars, and footers.

The block statement is used to identify the location in each file to establish inheritance. In the base template file {% block identifier %} and the {% endblock %} are used to identify what line to substitute the inherited content. Here the if statement and for loop is used for login authentication and flash messaging.

The if statements are used in templates to control the content that is returned from Flask routes, they are enclosed in blocks starting with {% if condition%} and closing with {% endif %}. The else and elif to further control the content.

For loops are used in templates to repeat content or iterate through data during Jinja2 template engine processing. For loops are contained inside statement delimiters with the following syntax {% for local_var in iterator %}. The content inside the block is repeated where the block starts with the for statement and is closed by {% endfor %}.

<!DOCTYPE html>
<html>
<head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
          integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">

    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}">
    <script src="https://kit.fontawesome.com/2a1d817ab7.js" crossorigin="anonymous"></script>

    {% if title %}
        <title>Flasky Blog - {{ title }}</title>
    {% else %}
        <title>Flasky Blog</title>
    {% endif %}
</head>
<body>
    <header class="site-header">
      <nav class="navbar navbar-expand-md navbar-dark cl-h fixed-top">
        <div class="container">
          <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarToggle"
                  aria-controls="navbarToggle" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
          </button>
          <div class="collapse navbar-collapse" id="navbarToggle">
              <div class="navbar-nav mr-auto">
                  <a class="nav-item nav-link" href="{{ url_for('main.home') }}" style="font-weight: 450;">News and Posts</a>
                  <a class="nav-item nav-link" href="{{ url_for('main.about') }}" style="font-weight: 450;">About</a>
                  <a class="nav-item nav-link" href="{{ url_for('users.contact') }}" style="font-weight: 450;">Contact</a>
              </div>

            <!-- Navbar Right Side -->
            <div class="navbar-nav">
            {% if current_user.is_authenticated %}
                <a class="nav-item nav-link" href="{{ url_for('posts.new_post') }}" style="font-weight: 450;">New Post</a>
                <a class="nav-item nav-link" href="{{ url_for('users.account') }}" style="font-weight: 450;">Account</a>
                <a class="nav-item nav-link" href="{{ url_for('users.logout') }}" style="font-weight: 450;">Logout</a>
            {% else %}
                <a class="nav-item nav-link" href="{{ url_for('users.login') }}" style="font-weight: 450;">Login</a>
                <a class="nav-item nav-link" href="{{ url_for('users.register') }}" style="font-weight: 450;">Register</a>
            {% endif %}
          </div>
          </div>
        </div>
      </nav>
    </header>
    <main role="main" class="container">
      <div class="row">
          <div class="col-md-8" style="margin-bottom: 50px">
              {% with messages = get_flashed_messages(with_categories=true) %}
                {% if messages %}
                    {% for category,message in messages %}
                        <div class="alert alert-{{ category }}">
                            {{ message }}
                        </div>
                    {% endfor %}
                {% endif %}
              {% endwith %}
              {% block content %}{% endblock %}
          </div>
          <div class="col-md-4">
              <div class="content-section" style="background-color: rgb(70, 103, 109);">
                  <h3 style="color: #fff;"><a href="{{ url_for('main.home') }}" class="hb">Flasky Blog</a></h3>
                  <h6 style="color: #fff;">Blog Website.</h6>
              </div>
          </div>
      </div>
    </main>


    <!-- Optional JavaScript -->
    <!-- jQuery first, then Popper.js, then Bootstrap JS -->
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
            integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
            integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
            integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
    <!-- <script type="text/javascript" src="{{ url_for('static', filename='index.js') }}"></script> -->
</body>
</html>

With current_user.is_authenticated and an IF-ELSE statement, we can ensure that only logged-in users will see a version of the navigation bar that lets them post an article or log out. Unauthorized users will see a navbar that lets them sign up or log in, but no option of contributing an article to the blog.

The base.html template is just a skeleton upon which every endpoint is fleshed out. With this in mind, it is easy to understand why its <main> element is not doing much, as this is where the specific job of each child template will come in. What little code we do have in the <main> element is quite important, as the {% block content %} statement allows each child template to inherit what is in base.html and avoid repetition.

Let's talk about how other pages will be able to inherit from it:

The index.html, about.html and contact.html files use basic HTML concepts and Jinja syntax standards that have been discussed earlier in this tutorial. Click each hyperlink to see the equivalent sample from Buzz Bite.
This is the Jinja syntax for inheriting from the base template as extensions into any of the child templates:

{% extends 'layout.html' %} {% block content %}
    <p> Sample HTML code for the specific webpage </p>
{% endblock content %}

As we have discussed earlier, The choice of blog design within main.css is entirely up to the programmer. There are many ways of doing this - including the popular Bootstrap styling toolkit with many handy tutorials online.

Extra Preparation

With most of the basic frontend and backend code now covered, we will focus on the more specific functions that this blog site will be able to perform.
For more detailed online tutorials and official documentation, with sample code being available on Buzz-Bite.

Imports

We already have all the necessary installations, so the next step is to make a few more imports which will become clearer with their use cases. It is faster to just type them at once, so this is how the top of our Python file should now look:

from flask import Flask, flash, render_template, url_for, request, redirect
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import current_user, login_user, logout_user, login_required, LoginManager, UserMixin
from flask_bcrypt import Bcrypt
from datetime import datetime
import os

The werkzeug.security module comes with Flask and provides password hashing for improved internet security, in case of a data breach. This means that we will never store the users' raw passwords, but rather a complex hashed form via generate_password_hash() which will only be decoded by check_password_hash() when a site visitor tries to log in.

To use the Flask-Login module, we pass our app as an instance of LoginManager():

login_manager = LoginManager(app)
bcrypt = Bcrypt(app)

User Authentication

A major function of this blog will be handling users and their data. This involves user registration and authentication. We will explore the Python code used to achieve said objectives in this section of the tutorial. To see the HTML templates used in our sample project, check out register.html and login.html on Ze Blog.

Registering a User

Our register() function collects user data from the registration form in the signup.html template. If the username is already taken, we will flash an error and reload the registration page. If the given username is available, we will add this new user's data to the "users" table in the database with a hashed password, using the User model. We will then redirect them to the login page.

@app.route("/register", methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated:
        return redirect(url_for('main.home'))
    form = RegistrationForm()
    if form.validate_on_submit():
        hashed_password = bcrypt.generate_password_hash(
                            form.password.data).decode('utf-8')
        user = User(firstname=form.firstname.data,
                    lastname=form.lastname.data,
                    username=form.username.data, email=form.email.data,
                    password=hashed_password)
        db.session.add(user)
        db.session.commit()
        flash(f"Hi {form.firstname.data + ' ' + form.lastname.data}, Your
               account has been created Successfully!",
              'success')
        return redirect(url_for('users.login'))
    return render_template('register.html', title='Register', form=form)

Log a User in

The login() function checks if the user's input on the login.html form matches any username and hashed password pair on the "users" table in the database. It then redirects to the homepage if the user is successfully authenticated, but flashes an error and reloads the login page if the data is invalid.

@app.route("/login", methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        return redirect(url_for('main.home'))
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user and bcrypt.check_password_hash(user.password,
                    form.password.data):
            login_user(user, remember=form.remember.data)
            next_page = request.args.get('next')
            return redirect(next_page) if next_page else 
                    redirect(url_for('main.home'))
        else:
            flash('Login Unsuccessful. Please check Email or Password', 
                    'danger')
    return render_template('login.html', title='Login', form=form)

We have now achieved how to authorize and authenticate our users, only allowing those who provide the correct name and password to have access to the blog.

Log a User out

The Flask-Login module provides an easy logout_user() function, which helps us revoke a user's access to protected routes once they click the logout link:

@app.route('/logout')
def logout():
    logout_user()
    flash("You have been logged out.")
    return redirect(url_for('index'))

Displaying posts

Another major feature of our blog will be handling articles on the site. This involves user authorization, as some features and routes should only be available to the author of the article. To see the relevant HTML templates used in our sample project, check out post.html and create_post.html on Buzz-Bite.

Display a single post

Let us start with an easy one. This article(id) function takes the article's ID as an argument, gets the requested article from the database, and then displays said article via the post.html template.

@app.route("/post/<int:post_id>", methods=['GET', 'POST'])
def post(post_id):
    post = Post.query.get_or_404(post_id)
    comments =Comments.query.order_by(Comments.id.desc()).all()
    image_file = url_for('static', filename='profile_pics/' + post.author.image_file)
    post.views += 1
    db.session.commit()
    return render_template('post.html', title=post.title, comments=comments, post=post, image_file=image_file)

Create a new post

A user has to be logged in to be able to create a post, as some user data is required to identify the author and later authorize them to edit or delete their articles.
This is where we will use an easy authentication tool provided by Flask-Login. The @login_required decorator does all the heavy lifting for us, as it ensures that a protected page will only be rendered if the current site visitor is logged in as a user. If the visitor is not logged in, it will display an error message.
Our new_post() function will enable authenticated users to post new entries to the "post" table in our database using the Post model, after getting relevant data from the form in create_post.html. We will then thank the user and redirect them to the homepage to see all articles.

@app.route("/post/new", methods=['GET', 'POST'])
@login_required
def new_post():
    form = PostForm()
    if form.validate_on_submit():
        post = Post(title=form.title.data, content=form.content.data, author=current_user)
        db.session.add(post)
        db.session.commit()
        flash('Your Post has been Created Successfully!', 'success')
        return redirect(url_for('index'))
    return render_template('create_post.html', title='New Post',
                           form=form, legend='New Post')

We should only allow authorized users to manipulate the articles in our database. While creating routes for editing and deleting articles, we will practice the concept of user authorization by writing IF statements that check that the currently logged-in user is a particular article's author. We will see these IF statements at work in the following steps

Update a Post

The update_post(post_id) function enables the author to update the post. It takes the post's ID as an argument, then gets the post's author from the database to compare with the logged-in user's username. If these are the same, it renders the create_post.html template for the user to edit their article, and then update the user's changes to the database. The function then loads the post's page if it has been successfully edited, but flashes an error and redirects home if the user is unauthorized to make changes.

@app.route("/post/<int:post_id>/update", methods=['GET', 'POST'])
@login_required
def update_post(post_id):
    post = Post.query.get_or_404(post_id)
    if post.author != current_user:
        abort(403)
    form = PostForm()
    if form.validate_on_submit():
        post.title = form.title.data
        post.content = form.content.data
        db.session.commit()
        flash('Your Post has been Updated Successfully!', 'success')
        return redirect(url_for('posts.post', post_id=post.id))
    elif request.method == 'GET':
        form.title.data = post.title
        form.content.data = post.content
    return render_template('create_post.html', title='Update Post',
                           form=form, legend='Update Post')

Delete a Post

Deleting a post does not require an HTML template, as it is a route that does one action and then redirects home. The function first queries the database for the entry whose ID matches the article ID it has received as an argument, then it checks if the current user is the author of the chosen article before executing the db.session.delete() function. We will then redirect to the homepage when the article is successfully deleted, or flash an error and redirect home if the user is unauthorized.

@app.route("/post/<int:post_id>/delete", methods=['POST'])
@login_required
def delete_post(post_id):
    post = Post.query.get_or_404(post_id)
    if post.author != current_user:
        abort(403)
    db.session.delete(post)
    db.session.commit()
    flash('Your Post has been Deleted!', 'success')
    return redirect(url_for('index'))

Add Comments

Our create_comment(post_id) function will enable authenticated users to add comment to a post and store it to the "comments" table in our database using the Comments model, after getting the necessary data from the form in post.html. The comment section does not require a new template because the comment section will be under every post.

@app.route("/create-comment/<post_id>", methods=['POST'])
@login_required
def create_comment(post_id):
    text = request.form.get('text')
    name = request.form.get('name')
    email = request.form.get('email')

    if not text:
        flash('Comment section cannot be empty.', category='error')
    else:
        post = Post.query.filter_by(id=post_id)
        if post:
            comments = Comments(text=text, author=current_user.id, name=name, email=email, post_id=post_id)
            db.session.add(comments)
            db.session.commit()
            flash('Your comment was posted successfully.', category='success')
        else:
            flash('Post does not exists', category='error')

    return redirect(url_for('post', post_id=post_id))

Create a like route

This route has the @login_required decorator which allows only logged-in users to access this page. The code in the function below lets users like and unlike any post in the blog application. This route does not require any template.

@app.route("/like-post/<post_id>", methods=['GET'])
@login_required
def like(post_id):
    post = Post.query.filter_by(id=post_id)
    like = Likes.query.filter_by(author=current_user.id, post_id=post_id).first()

    if not post:
        flash("Post does not exists.", category='error')
    elif like:
        db.session.delete(like)
        db.session.commit()
    else:
        like = Likes(author=current_user.id, post_id=post_id)
        db.session.add(like)
        db.session.commit()

    return redirect(url_for('post', post_id=post_id))

We have completed this project by designing our blog site and adding all desired features with clean code. Congrats! But there is one more thing...

Requirements File

It is recommended practice to use a requirements.txt . First, it allows you to keep track of the Python modules and packages used by your project. It simplifies the installation of all of the required modules on any computer without having to search through online documentation or Python package archives. It is used to install all of the dependencies on another computer so that they are compatible with one another. Second, it makes it easy to share your project with others. They install the same Python modules you have listed in your requirements file and run your project without any problems.

> (env) C:\Users\User\flasky_blog> pip freeze > requirements.txt

This file can then be accessed by anybody who wants to use our web app, as they can install all our packages directly with one command:

> (env) C:\Users\User\flasky_blog> pip install -r requirements.txt

Let's See how the real blog website looks like

Conclusion

Thanks for exploring the beautiful simplicity of Flask with me! 💜

We have learned how to set up a Flask environment in VS Code, manipulate HTML templates with Jinja, and perform CRUD operations on a database with SQLite and SQLAlchemy. We have also practiced responsive web design with HTML and CSS, routing, password hashing with Werkzeug, message flashing, internet security, user authentication, and user authorization.

Please share this tutorial with fellow WebDev enthusiasts, especially those interested in Backend Software Engineering with the Python Flask framework.

Check out Buzz-Bite, a project I built using these principles for the second-semester exam at AltSchool Africa.

Follow me on Github and Twitter for more insights from my tech journey.

Cheers to more coding and making magic!

Credits

Thanks for reading the blog and I hope it helped you! Cheers to more coding...