Do you own your blog? If you have posted your blog on LinkedIn, Blogspot, Wordpress, Medium and the likes - I'd argue you don't fully own your blog! To me personally, ownership means many things
How do you achieve this? Well, it's not really hard if you already know how to write HTML and CSS. You just generate pages after pages in HTML and host them wherever you want to host them! But that is not efficient for a number of reasons:
What's the solution? If you knew a tool like Django or Flask (a web application framework) you could write simple programs to generate these pages in a more efficient manner. But if you want to host these applications in the public domain, you need a hosting service and that costs money! What if you could ask your web application framework to generate a static set of HTML pages that you could put anywhere you wanted? That's the best of both worlds - that's what Flask Frozen does. And if you want the liberating experience of writing a blog that you completely own and are willing to pay a little bit of price in terms of learning some new tech for it - you are reading the right tutorial!
NOTE: The working source code for this entire tutorial is available at the following Github repository.
Let's do the usual thing of creating a separate folder for our project and initialize our own virtual environment in it.
mkdir flask-blog-tutorial cd flask-blog-tutorial python3 -m venv venv/ source venv/bin/activate pip install flask frozen-flask flask-flatpages
Just to clarify, we have installed three main packages in our new virtual environment
Next, we are going to make two folders - one for our 'static' content - this includes any CSS files, images, JavaScript files and anything else that does not change; the other for our 'templates' which are blueprints for manufacturing our final pages.
mkdir templates mkdir static
Here is the basic philosophy of desiging a flask application in a super condensed form.
Flask()
object, usually called app
.app.run()
starts a light weight HTTP server so that you can test your
application without having to setup a full fledged production server.Simple enough? Let's do this. Open a file called sitebuilder.py
and add the
following code in it.
import sys from flask import Flask, render_template DEBUG = True app = Flask(__name__) app.config.from_object(__name__) @app.route('/') def index(): return render_template('index.html') app.run(host='0.0.0.0', port=5001)
What's happening here? We have done exactly what we mentioned in the basic
philosophy of how Flask works. The @app.route()
decorator is binding the
function index()
to the root URL /
. And it will create a page using
index.html
which we are yet to define.
Since the template files don't exist, the above code cannot work. Let's fix
this. Inside the templates
directory we created earlier, create a new file
called base.html
and write the following in it.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title> My Flask Blog</title> </head> <body> <header> <h1> My Common Header</h1> </header> {% block content %} {% endblock %} </body> </html>
This is your standard HTML syntax - nothing new here except for the {% block
content %}
and {% endblock %}
tags. This is part of the Jinja2 template
syntax which essentially is a way of saying "this is a block called 'content'
and we are going to replace it in the future with something else". This will
become clear immediately below.
Now, create another file called index.html
. And place the following content in
it.
{% extends "base.html" %} {% block content %} <p> Welcome to my blog! Enjoy.</p> {% endblock %}
Here is what we are saying here - the index.html
will just look like
base.html
except that the block 'content' will be replaced with whatever we
are putting in this file. This is called 'template inheritance'.
Do you see the power of this approach? Anything common to all pages on your
blog - the headings, the footers, the navigation bars, the CSS etc. can now be
placed inside base.html
and the changes will reflect on all other pages so
long as they 'extend' this base.html
. So, no more messy multiple changes when
you decide to change your layout or add a new navigation bar item.
Time to give this a spin. On your command line, say
python sitebuilder.py
Use your browser to visit http://127.0.0.1:5001/
and see your page in action!
If you feel like, try making changes to your HTML files and see what happens.
Now, how do you write a blog post and make it accessible from a URL? We shall
tackle this now. Let's go to our sitebuilder.py
and add the following lines.
from flask_flatpages import FlatPages FLATPAGES_AUTO_RELOAD = DEBUG FLATPAGES_EXTENSION = '.md' pages = FlatPages(app) @app.route('/<path:path>.html') def page(path): page = pages.get_or_404(path) return render_template('page.html', page=page)
Let's dissect this carefully.
.md
- so
any other file with any other extension is not considered by Flaskapp
object to the FlatPages
constructor we are indirectly
providing all the information about our app that is needed to locate the pages
we create and render them.something.com/abc.html
. The abc
part will be extracted and passed
down to the view function.pages
object constructed
by FlatPages
to query for a page matching the name path
.Phew! That's a lot. Let's dwelve a bit further. Let's temporarily modify our
page()
function to say
def page(path): print("Page function running") page = pages.get_or_404(path) return render_template('page.html', page=page)
We have just added a print()
statement to understand the workflow. Now, in
your browser type http://127.0.0.1:5001/first.html
, you will get a 404 (Not
Found) error. This is expected. But check out the terminal - the message "Page
function running" should appear indicating that this function was correctly
matched to the URL. But since the page did not exist, a 404 response was
returned. So, let's fix that.
$ mkdir pages
Inside pages, create a file called first.md
and type the following
\\ blank line \\ blank line # My First Blog Here is some content for this blog. Written in Markdown. - Bullet point 1 - Bullet point 2 - Bullet point 3
Note: The two blank lines at the top of the file are deliberate and needed.
Don't type the \\ blank line
part - but keep the lines empty at the top of the
file.
And now, refresh our 404 exhibiting browser page. You should now get an error
saying TemplateNotFound
. This means it has found the correct entry in the
pages
object but was not able to find page.html
file. So, here is the
lesson - the way we have setup things, an attempt to visit /first.html
on our
site causes a search for first
inside pages
which in turn can exist only if
pages/first.md
exists. Remember .md
is because we configured
FLATPAGES_EXTENSION
variable as .md
.
It's easy to fix the TemplateNotFound
error. Just create a page.html
inside
templates
directory and type in the following.
{% extends "base.html" %} {% block content %} {{ page.html|safe }} {% endblock %}
Most of this should be clear now except the page.html|safe
. This is what's
happening here.
page
is an object containing an attribute called html
which stores the
HTML code obtained by parsing the Markdown present in our file first.md
.|safe
essentially tells Flask (or Jinja) that this HTML is 'safe' - it does
not need to modified in any way. (Else all tags will be ignored and converted
to safe HTML representations and your blog post will contain HTML tags! Try
removing and see what happens!)Now, refresh our page in the browser again. And you should see your blog! Woohoo!
Actually, .md
files are not pure Markdown files. They are special syntax files
which contain Markdown. This is why we had to put two empty lines at the top.
These two empty lines separate the metadata of the file from the actual content.
So, what we will do now is to modify first.md
to read as follows.
title: My First Blog date: 2020-02-23 category: general Here is some content for this blog. Written in Markdown. - Bullet point 1 - Bullet point 2 - Bullet point 3
Now, the three items - title
, date
and category
are available as
attributes of the page
object inside the page.html
template. We can modify
the template to read as follows.
{% extends "base.html" %} {% block content %} <h1> {{ page.title }} </h1> <small> Published on {{ page.date.strftime('%A, %d %B, %Y') }} </small> <br> {{ page.html|safe }} {% endblock %}
See how we are now rendering the title of the blog using page.title
. Also,
notice that we using strftime
function available with Python datetime
objects for deciding how the date will be displayed. Below is a screenshot of
what the blog page would look like at this stage.
Great! You can now creating second.md
, third.md
and whatever else suits your
fancy inside the pages
directory and access them using URLs such as
/second.html
, /third.html
. And more. If you are like me and like to
organize blogs in multiple categories, just create sub-folders inside pages
directory and the URLs will change to /subfolder/first.html
and so on.
What we have done so far is to have a web application that is running functions according to URLs and these functions are responsible for generating the pages. But as mentioned at the beginning of the tutorial, hosting this application can be tricky. What we want to do is to deploy plain HTML pages. The process of converting 'dynamic' pages (which are generated on the fly) into 'static' HTML pages is what we call 'freezing' here. Let's do this.
Add the following lines to sitebuilder.py
at suitable places.
from flask_frozen import Freezer from flask import url_for freezer = Freezer(app) @freezer.register_generator def pagelist(): for page in pages: yield url_for('page', path=page.path)
Replace the app.run(...)
line with the following piece of code.
if __name__ == '__main__': if len(sys.argv) > 1 and sys.argv[1] == 'build': freezer.freeze() else: app.run(host='0.0.0.0', port=5001)
Let's review the essential parts of this code.
app
to Freezer - this allows it to fetch whatever information
it needs about the app in order to do its job.pages
object.freezer.register_generator
so that among the
things Flask Freeze has to freeze, this will include all our pages.sitebuilder.py
application - one where we
run it simply by saying python sitebuilder.py
and we can use this to see how
our pages look like as we change them in real time. And another mode where we
require that our new set of static pages be generated.To see the freezing in action, terminate the development server if running and type in the following command.
$ python sitebuilder.py build
Ignore the warnings that appear. Head to the build
folder and you will find
the HTML pages generated there! Job well done.
If you take all the files inside build
and place them where the root folder of
your site is configured, it's done - you have published your blog!
If you don't have a hosting server with a document root configured for you, here is what you can do
<yourUserName>.github.io
build/
files inside this repository (see Git and Github help
manuals for learning how to do this.)Visiting <yourUserName>.github.io
should display your pages.
While not a part of the original goal laid out at the beginning, let us cover
something which is obvious - having a list of all your posts displayed on the
index.html
page.
Change the index()
function to the following:
@app.route('/') def index(): return render_template('index.html', pages=pages)
And add the following to your index.html
.
<h3>Contents</h3> <table border="2px"> <thead> <tr> <td> Title </td> <td> Link </td> </tr> </thead> {% for page in pages %} <tr> <td> {{ page.title }}</td> <td> <a href="{{ page.path }}.html"> Read </a></td> </tr> {% endfor %} </table>
What did we do?
index()
view function to pass the pages
object to the
template.pages
object and populate table rows
containing the blog post title and a hyper link. Note that since the path does
not contain .html
inside it, we add it manually.Below is a screenshot of what the index page looks like, assuming a single blog post.
There is much more hard work that needs to be done to achieve a fully functioning and ready to publish blog. Here is what I recommend as future course of action.
NOTE: The working source code for this entire tutorial is available at the following Github repository.