Create a Static Blog Using Python Flask
Tags: python, webdev
Published: 2020-08-11 20:18:17

# Why flask?

I wanted to be able to launch a blog quickly with python in the most simple way that I could without using any of the usual tools like wordpress of blogger.  After doing some research I came across `flask_flatpages` and `flask_frozen` and set to work.

`flask_flatpages` allows us to write our blog posts in .md files, allowing us to focus on the content rather than html tags, while `flask_frozen` 'flattens' our website into static `.html` files which we can upload to any host without and hassle.  Since we use git to update our repository adding a new blog post is as simple as writing out post in a new `.md` file, freezing the app, and updating the repository.

The intention of this article is to cover the following:

- [Setting up the flask application](#setting-up-the-flask-application)
- [Using flask_flatpages to generate our blog posts](#using-flaskflatpages-to-generate-our-blog-posts)
- [Using flask_frozen to generate static html files](#using-flaskfrozen-to-generate-static-html-files)
- [Hosting on github.io](#hosting-on-githubio)
- [Deploying using git](#deploying-using-git)
- [Setting up a custom domain](#setting-up-a-custom-domain)
- [Registering the site with Google Analytics](#registering-the-site-with-google-analytics)
- [Summary](#summary)


<h1 id="setting-up-the-flask-application">Setting up the flask application</h1>

As always I start with setting up a new virtual environment for my project

    cd projects
    mkdir blog
    cd blog
    virtualenv -p python3 venv
    source venv/bin/activate

Followed by installing the dependencies that I know I'll need using `pip install`

    pip install flask flask_flatpages flask_frozen

That's it.  Now we have everything we need to get started.

Now in a new file `app.py` we need some boilerplate flask code.  Mine is slightly different to the one on the flask quickstart guide but I'll explain why.

    from flask import Flask, render_template, url_for
    from flask_flatpages import FlatPages
    from datetime import datetime
    
    app = Flask(__name__)

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


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

The `if __name__ == "__main__":` statement is there to allow us to easily run the development server from the console using the command `python app.py`.  It's not essential but it just makes life easier.  And since we are going to be generating a static site using this flask app, we dont need to worry about having debug set to true as we will not be deplying the flask app it's self.

So to test that this works just start up the development server with `python run.py` and open the url that is given to us in the console, something like `http://127.0.0.1:5000/`. We should see a blank webpage with just the words "Hello, World!" at the top.  If we do, everything worked and we have a working flask app.  But it doesn't do much yet, so let's get to the meaty bits.


<h1 id="using-flaskflatpages-to-generate-our-blog-posts">Using flask_flatpages to generate our blog posts</h1>

Writing out posts is going to be a lot easier if we write each blog post in a .md file. The job of `flask_flatpages` is to take these `.md` files and essentially tell flask to serve them in the browser as `.html`.  

To start we need to tell `flask_flatpages` to look for files with the extension `.md`.  By default it will look for these in `/pages/` directory. We're okay with that so we will leave it as is.  However also by default it will be looking for `.html` files instead of `.md`.  So let's add some configuration and instantiate a `FlatPages()` class, passing in our `app` instance.  (also don't forget to import Flatpages).  

    from flask import Flask, render_template, url_for
    from flask_flatpages import FlatPages
    from datetime import datetime

    app = Flask(__name__)
    app.config['FLATPAGES_EXTENSION'] = '.md'

    pages = FlatPages(app)

To keep thing simple we'll delete the route in our flask app and add our own.  We will add another route later to serve as a home page but for now we just want to deal with our blog posts. We do this by replacing the route we already have with the following:

    @app.route('/<path:path>.html')
    def page(path):
        page = pages.get_or_404(path)
        return render_template('page.html', page=page)

This route searches for a file in our `pages/` directory and if it exists, serves it to our browser as `.html`.  If it doesn't exist it will return a 404 error. So for example if we try to follow `http://127.0.0.1:5000/blogpost.html`, the route will look for a file called `blogpost.md` in the pages directory.  

This is useful because it means that we can just put all of our blog posts into the `pages/` directory and our flask app will be able to serve them up as and when we need, without us having to explicitely add all of the individual URLs to our app.

Now to see the content of our pages we need to set up a couple of html files.  Anyone who has used flask to build a web app before will be aware that we do this by using templates.  So let's make a new directory called `templates` and add `templates.html`.  This template will dictate how the whole website looks, along with it's accompanying .css file, but setting up that is a whole other guide. all we need to know now is that wherever in our `templates.html` file we want to show the blog post content we should simply add:
```
    {% block content %}
    {% endblock %}
```
After that it's as simple as creating another html file that will contain just the content of our blog post.  We'll call this `page.html` and it will contain the following:
```
    {% extends "template.html" %}
    {% block content %}
        <h1>{{ page.title }}</h1>
        <p>{{ page.html|safe }}<p>
        <strong>{{ page.author }}</strong> {{ page.date }}</span>
    {% endblock %}
```
The bits of code in the curly brackets will be substituted with the meta data from the top of our .md files thanks to jinja (an engine used by flask). We can grab these variables because we passed our page into the template in our route earlier with `page=page`.  We define our meta variables at the top of our `.md` file very easily like so (remembering to leave a 2 line gap between the meta and content)
```
    title: Post title goes here
    date: 19 Jun 20
    author: Me! :)
    description: This is a summary of the post


    # This is the start of my content!
```
Hopefully that should be it!

All we are missing now is a home page which contains a list of our blog posts. We will want to display these posts in reverse order by date, so we can see the most recent first.  We do this by using a lambda function, which can be quite tricky to understand so here's a <a href="https://www.w3schools.com/python/python_lambda.asp" target="_blank">link to how it works</a>.  But all we need to know for now is that it does work!  That being said, let's add a new route to the app.
```
    @app.route('/')
    def index():
        posts = [p for p in pages if "date" in p.meta]
        sorted_pages=sorted(posts, reverse=True, key=lambda page: datetime.strptime(page.meta["date"], "%d %b %y"))
        return render_template('bloghome.html', pages=sorted_pages)
```
Change the `"%d %b %y"` to reflect your preferred way of writing the date in your blog post meta data.  More information on this formatting can be found <a href="https://www.w3schools.com/python/python_datetime.asp" target="_blank">here</a>.

We need a `bloghome.html` in our templates folder now which we can use to display our list of blog entries.  In this file we'll use a for loop to iterate over each of our pages like this:
```
    {% extends "template.html" %}
    {% block content %}
        {% for page in pages %}
        <a href="{{ page.path }}.html"><h2>{{ page.title }}</h2></a>
        <p>{{ page.description }}</p>
        <p>{{ page.date }}</p>
        <hr>
        {% endfor %}
    {% endblock %}
```
Simple as that.  You should now be able to navigate from your blog homepage to the blog posts.  If you want to add a link back to the homepage make sure you add it in `template.html` so it appears in all of your pages.

```
<h1 id="using-flaskfrozen-to-generate-static-html-files">Using flask_frozen to generate static html files</h1>
```
The hard stuff is out of the way now, all this next part does is to freeze our website to `.html` files, making it much easier to deploy.  In the interest of simplicity we'll impliment this functionality in a new file, `freeze.py` with the following code:
```
    from flask_frozen import Freezer
    from flask import url_for
    from app import app, pages


    app.config['FREEZER_DESTINATION_IGNORE'] = ['.git*', 'CNAME']
    freezer = Freezer(app)

    @freezer.register_generator
    def pagelist():
        for page in pages:
            print(f"making page for {page.path}")
            yield url_for('page', path=page.path)

    if __name__ == "__main__":
        freezer.freeze()
```
Notice the app config here. At this stage we dont actually need this line, but it comes in handy later on when we want to add more posts and push them to our github repo. Whenever we freeze by default the created files will be written to a new directory called `/build/`. In doing this anything in the build folder will be deleted first. The CNAME is a file required later for our custom URL, and the .git* allows us to use this as our origin.  This config simply tells flask_frozen to ignore these files, and to not delete them.

All there is to do now is to run freeze.py in the terminal with:
```
    python freeze.py
```
Simple! Now we should have all of our required files in the `/build/` directory.

It is important to note though that if you open the newly created files in the browser the links back to the root won't work properly.  But that is because they are doing just that, linking to `/`.  Once these files are uploaded to the root folder of your host (i am using <a href="https://github.io/" target="_blank">github.io</a>) the links should work corrently.


<h1 id="hosting-on-githubio">Hosting on github.io</h1>

This is super simple.  If you don't have a github account create one, and make a new repository.  Name your repository the following:
```
    <your github username>.github.io
```
make sure you don't include the `README.md`. We want our webpage to load first instead of this.

That's it!  Nothing more to say.


<h1 id="deploying-using-git">Deploying using git</h1>

Again, another super simple step.  I won't teach people how to suck eggs so here are the commands I used to add the files to my github repo.

    cd <build directory>
    git init
    git remote add origin <repository url>
    git add .
    git commit -m "First commit"
    git push origin master

If all has gone well you should be able to see your new blog by following the link:
```
    http://<your github username>.github.io
```
From now on if you add a blog post to your project, all you need to do from your app directory is:
```
    python freeze.py
    cd build/
    git add .
    git commit -m "commit comment"
    git push origin master
```
One day we could work on automating this step but for now we have the functionality!

The rest of this blog is optional, but for registering a custom domain and setting up Google Analytics, read further.


<h1 id="setting-up-a-custom-domain">Setting up a custom domain</h1>

There are pleanty of providers for domains, but I used <a href="https://godaddy.com/" target="_blank">godaddy.com</a>.  Once you've puchased the domain you want go ahead and make a new file in your repository root folder called `CNAME` and write the domain you have just bought in the file.

Next go to your domain provider's website and look for the DNS settings, on Godaddy this is on your active products page.  You should see a table of records.  Edit the record with Type `CNAME` and Name `www`. Change the value to `<your github username>.github.io`.

Last step is to add four more records. So click `ADD` and enter the Type as `A` and the Name as `@`.  You are going to want to carry out this step four times, each time entering one of the following into Value:
```
    185.199.108.153
    185.199.109.153
    185.199.110.153
    185.199.111.153
```
<strong>Note that this is specific to github.io</strong>

Back to github now.  In your repository settings if you scroll down you should see a tick box asking if you want to push https.  It might take a while before you can tick it but once you can go for it.

If you have any problems at this stage it might be worth visiting the help section linked <a href="https://help.github.com/en/github/working-with-github-pages/configuring-a-custom-domain-for-your-github-pages-site" target="_blank">here</a> to look for a solution.

Cool. we're done. Google Analytics next!

```
<h1 id="registering-the-site-with-google-analytics">Registering the site with Google Analytics</h1>
```
Another super simple step here.  Create an account with <a href="https://analytics.google.com/analytics/web/" target="_blank">Google Analytics</a>.  Once done, on your dashboard you'll see some code that should be copied into your website's html.  So copy this code and copy it into your `template.html` directly under the `<head>` tag.

Done.


<h1 id="summary">Summary</h1>

If everything has worked you should have a working blog now.  Thanks for taking the time to read, and if you have any comments or suggestions feel free to contact me.  My twitter handle is <a href="http://twitter.com/arrantate" target="_blank">@arrantate</a>.

This blog post was created on my 5th day of #100daysofcode challenge and will hopefully be the first of many.  I found out about this method of creating a blog <a href="https://vkaustubh.github.io/blog/geek/2020-02-23-blogging-with-flask.html" target="_blank">here</a>.  I encountered a few problems but managed to figure them out using the following documentation and repositories as reference:

- <a href="https://flask-flatpages.readthedocs.io/en/v0.7.1/" target="_blank">Flask-FlatPages — Flask-FlatPages 0.7.1 documentation</a>
- <a href="https://pythonhosted.org/Frozen-Flask/" target="_blank">Frozen-Flask — Frozen-Flask 0.15 documentation</a>
- <a href="https://github.com/insomnux/flaskblog" target="_blank">insomnux / flaskblog</a>