It’s been a long time I wanted to federate my online identities in a single, managed place — hence the website you’re currently browsing. I’ve also been looking for a static website builder for some times, trying many and retaining zero. It was a bit depressing, frustrating to say the least.
Then I encountered this tweet by Armin Ronacher:
Frozen-Flask is really, really useful. Should have used that earlier.
— Armin Ronacher (@mitsuhiko) February 6, 2012
Armin is the author of Flask, a Python microframework I much appreciate for its simplicity, so this tweet immediately rang a bell and I started exploring the possibilities offered by Frozen-Flask.
Frozen-Flask basically freezes a Flask application into a set of static files, so you can host it without pain, but with speed. Combined with Flask-FlatPages, you got the perfect set for generating your static website with everything you could expect by using a framework:
- cool uris & easy routing management
- powerful templating
- local, dynamic serving
- static version generation
First iteration: project setup
Create a brand new virtualenv in a new directory and install the necessary packages using pip:
$ mkdir sample_project && cd !$
$ mkvirtualenv --no-site-packages `pwd`/env
$ source env/bin/activate
$ pip install Flask Frozen-Flask Flask-FlatPages
Write a very first version of our app in a sitebuilder.py
file:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "Hello World!"
if __name__ == "__main__":
app.run(port=8000)
Run it; you should see someting like:
$ python sitebuilder.py
* Running on http://127.0.0.1:8000/
* Restarting with reloader
Ensure with your browser that everyting is going fine by heading at http://127.0.0.1:8000/
.
New iteration: adding flat pages
Flask-FlatPages provides a collection of pages to your Flask application. Pages are built from flat text files as opposed to a relational database.
Create a pages/
directory at the root of your project folder, and put a hello-world.md
file in there:
$ mkdir pages
$ vi pages/hello-world.md
The pages/hello-world.md
:
title: Hello World
date: 2012-03-04
**Hello World**, from a *page*!
As you can see, we can write plain Markdown for our page contents. So let’s rewrite our app to serve any flatpage by its filename:
from flask import Flask
from flask_flatpages import FlatPages
DEBUG = True
FLATPAGES_AUTO_RELOAD = DEBUG
FLATPAGES_EXTENSION = '.md'
app = Flask(__name__)
app.config.from_object(__name__)
pages = FlatPages(app)
@app.route('/')
def index():
return "Hello World"
@app.route('/<path:path>/')
def page(path):
return pages.get_or_404(path).html
if __name__ == '__main__':
app.run(port=8000)
Now requesting http://127.0.0.1:8000/hello-world/
will display our flatpage. Note that the markdown source is converted to html by getting the html
property of our page object.
New iteration: adding templates
Flask uses the Jinja2 template engine, so let’s create some templates to decorate our pages. First create a templates
directory at the root of the project:
$ mkdir templates
Create a base layout in templates/base.html
:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>My site</title>
</head>
<body>
<h1><a href="{{ url_for("index") }}">My site</a></h1>
{% block content %}
<p>Default content to be displayed</p>
{% endblock content %}
</body>
</html>
Note the use of the url_for()
template helper, that’s the way we generate urls using Flask and Jinja2.
Now the page.html
template to fill this layout with page contents:
{% extends "base.html" %}
{% block content %}
<h2>{{ page.title }}</h2>
{{ page.html|safe }}
{% endblock content %}
Our app is now:
from flask import Flask, render_template
from flaskext.flatpages import FlatPages
DEBUG = True
FLATPAGES_AUTO_RELOAD = DEBUG
FLATPAGES_EXTENSION = '.md'
app = Flask(__name__)
app.config.from_object(__name__)
pages = FlatPages(app)
@app.route('/')
def index():
return "Hello World"
@app.route('/<path:path>/')
def page(path):
page = pages.get_or_404(path)
return render_template('page.html', page=page)
if __name__ == '__main__':
app.run(port=8000)
Hell, what did we just do?
- we created templates for our app; a common layout (
base.html
) and a page template (page.html
) - we used the
render_template
function to use the page template for decorating our pages - the page template extends the base one to avoid copying and pasting stuff for every page on Earth
New iteration: display the list of available pages on the homepage
For now our homepage has been a bit sick, to say the least. Let’s make it an index of all available pages.
Create a templates/index.html
with the following contents:
{% extends "base.html" %}
{% block content %}
<h2>List of stuff</h2>
<ul>
{% for page in pages %}
<li>
<a href="{{ url_for("page", path=page.path) }}">{{ page.title }}</a>
</li>
{% else %}
<li>No stuff.</li>
{% endfor %}
</ul>
{% endblock content %}
Feel free to create more flat pages, the same way we did with hello-world.md
, storing the file into the pages/
directory using the .md
extension.
So the index()
route of our app is now:
@app.route('/')
def index():
return render_template('index.html', pages=pages)
If you reload the homepage, a list of all available flatpages is displayed with a link for each. Damn, that was pretty easy.
New iteration: adding metadata to pages
Flask-FlatPages allows to enter metadata for pages the same way we did for the title and the creation date with our hello-world.md
, and access them using the page.meta
property, which contains a plain silly python dict. That’s pretty awesome when you think about it, heh?
So let’s imagine you want tags for your pages, our hello-world.md
becomes:
title: Hello World
date: 2012-03-04
tags: [general, awesome, stuff]
**Hello World**, from a *page*!
For the records, metadata are described in YAML, so you can use strings, booleans, integers, floats, lists and even dicts which will be converted to their respective native Python equivalent.
We’re going to use two different templates for listing general pages and tagged ones, using a shared template partial. Our index.html
is now:
{% extends "base.html" %}
{% block content %}
<h2>List of stuff</h2>
{% with pages=pages %}
{% include "_list.html" %}
{% endwith %}
{% endblock content %}
Create a new tag.html
template, which will display the list of tagged pages:
{% extends "base.html" %}
{% block content %}
<h2>List of stuff tagged <em>{{ tag }}</em></h2>
{% with pages=pages %}
{% include "_list.html" %}
{% endwith %}
{% endblock content %}
The new _list.html
template we need for inclusion contains:
<ul>
{% for page in pages %}
<li>
<a href="{{ url_for("page", path=page.path) }}">{{ page.title }}</a>
{% if page.meta.tags|length %}
| Tagged:
{% for page_tag in page.meta.tags %}
<a href="{{ url_for("tag", tag=page_tag) }}">{{ page_tag }}</a>
{% endfor %}
{% endif %}
</li>
{% else %}
<li>No page.</li>
{% endfor %}
</ul>
Let’s add a new tag
route to our app, to use our new tag.html
template:
@app.route('/tag/<string:tag>/')
def tag(tag):
tagged = [p for p in pages if tag in p.meta.get('tags', [])]
return render_template('tag.html', pages=tagged, tag=tag)
Note: if you didn’t like Python’s list comprehensions yet, now you do.
New iteration: generate the static stuff
Well, for now we only have a dynamic website, which uses and serves flatpages stored on the filesystem: CRAP. But the idea’s of course not to host a Flask app but a set of static files and assets to skip the need of any application server.
Here enters Frozen-Flask. Its use is damn easy:
import sys
from flask import Flask, render_template
from flask_flatpages import FlatPages
from flask_frozen import Freezer
DEBUG = True
FLATPAGES_AUTO_RELOAD = DEBUG
FLATPAGES_EXTENSION = '.md'
app = Flask(__name__)
app.config.from_object(__name__)
pages = FlatPages(app)
freezer = Freezer(app)
@app.route('/')
def index():
return render_template('index.html', pages=pages)
@app.route('/tag/<string:tag>/')
def tag(tag):
tagged = [p for p in pages if tag in p.meta.get('tags', [])]
return render_template('tag.html', pages=tagged, tag=tag)
@app.route('/<path:path>/')
def page(path):
page = pages.get_or_404(path)
return render_template('page.html', page=page)
if __name__ == '__main__':
if len(sys.argv) > 1 and sys.argv[1] == "build":
freezer.freeze()
else:
app.run(port=8000)
And run:
$ python sitebuilder.py build
Open the build
folder generated by this command:
$ tree
.
├── hello-world
│ └── index.html
├── index.html
└── tag
├── awesome
│ └── index.html
├── general
│ └── index.html
└── stuff
└── index.html
5 directories, 5 files
MIND: BLOWN.
You can now deploy the contents of this build
directory to any webserver which’s able to serve static files, and you’re done. With just 34 lines of manually written Python code… not bad heh?
Of course, our website is pretty crappy right now, but you should be get started to add features on your own, now.