Frictionless Web Development with Flask Static Sites with a Lightweight Python Stack.
For a while I’ve been looking for a more fluid toolkit for rapidly developing and iterating static sites. I have three use cases in mind: client html mockups with rapid iterations or slight variations, maintaining the static client sites that I have in the stable, and hacking on my own site. In each of these cases, dealing with databases, CMSs, versioning, or directories of HTML files was either too heavy, too slow, or both. My goals are straightforward:
- use python as much as possible.
- develop dynamically, publish statically
- use a real text editor for writing content instead of a text field on a web page
- be easy to add big types of content (“I want a new section for all of my music”) and trivially easy to add small bits of content to existing pieces.
- be easy to create a template and trivially easy to extend an existing template with slightly different content.
- all content and templates are all static and easy to version with Mercurial
- no databases, database schemas, or migrations and no CMSs to install, move, or memorize.
- ability to push an update out to the server with one terminal command.
First, A Diversion
Web development can become a chore, with too little time actually creating things and too much time learning APIs, stitching them together, and trying to remember their syntax. when I’m making a Django site, I feel like I’m programming in Django instead of Python. When I using JQuery, I feel like I’m programming JQuery instead of Javascript. Of course, frameworks and higher level APIs have enormous value (and the situation improves dramatically with one’s familiarity and excellent documentation). But I want to use simpler, more expressive tools even if this means reinventing a few wheels. Something light that allows me to structure my apps the way I want. Why? Because it’s more fun to make things than stitch APIs together.
Flask, As Simple As You Want To Be
Flask is a relatively new project that is part of the nice growing collection of packages under the Pocoo banner. It is a lightweight wrapper of two of their other projects, Werkzeug (a WSGI toolkit), and the excellent Jinja 2 template library.
Flask shares a quality with Python in general and the best of the core and third party python packages: it is as simple as you want it to be. The learning curve is very small. If you are just using the core functionality, the API is simple and intuitive. It grows with you as your needs grow. As your project grows, you can break it out from a single file to a whole package.
The quickstart guide is a great overview (and the rest of this post assumes a basic familiarity). I love its flexibility. You can start with all the content in your HTML files and break out reusable bits as needed. Or you can put the content in yaml files or a database and break things apart into an MVC structure. Whatever works for you.
Flask assumes very little about your file structure. By default, it looks for templates in the templates/
directory. And when you are using the dev server, it will automatically serve static content (images, css, etc) from static/
directory. Other than that, you basically put things where you want them.
For most static projects and mockups, the templates are where the action is. When I started making Django sites after working with Wordpress and Expression Engine, the Django templating system was a revelation. The template inheritance, block structures, and filters were exactly what I had been hoping for. Jinja was inspired by the Django template system and contains a host of improvements. The template syntax is straightforward and discoverable.
YAML == Easy content
I find myself using YAML more and more these days. My projects will often have configuration inputs or outputs that naturally lend themselves to plain text files. And since the excellent PyYAML package automatically translates them into base python objects, YAML is often the most expressive and lightweight way to deal a wide range of data. Consider a YAML file for a blog post
:::yaml
title: "The Gettysburg Address"
author: "Abe Lincoln"
date: 2011-01-08
banner: "/path/to/photo.jpg"
body: |
Four score and seven years ago our fathers brought forth
on this continent, a new nation, conceived in Liberty, and
dedicated to the proposition that _all men_ are created equal.
Now we are engaged in a great civil war, testing whether
that nation, or any nation so conceived and so dedicated,
can long endure. We are met on a great battle-field of that
war. We have come to dedicate a portion of that field, as a
final resting place for those who here gave their lives that
that nation might live. It is altogether fitting and proper
that we should do this.
tags:
- presidents
- speech
- civil war
gallery:
- pic: '/path/to/pic'
caption: 'A very descriptive thingy'
- pic: '/path/to/another/photo'
caption: 'An even better caption'
In this example, pyyaml would return a dictionary of string objects (title
, author
, banner
), a datetime object (date
), a list (tags
), and a list of dictionaries (gallery
). The API is very straightforward.
:::python
import yaml
mypost = yaml.load(file('path/to/file', 'r'))
print(mypost['title']) # 'The Gettysburg Address'
Even better with Markdown and Syntax Highlighting
YAML is more nutritious with Markdown. Markdown enables you to write big blocks of content and easily output valid HMTL. There is a robust Python implementation:
:::python
import markdown
foo = "# hello there, **Mr Bond**."
foo_html = markdown.markdown(foo)
print(foo_html)
# <h1>hello there, <strong>Mr Bond</strong>.</h1>
Need code highlighting? Searching around, there is a host of outdated information on how to plumb everything together. But these days, Python-Markdown comes bundled with an extension for Pygments called codehilite
which enables code highlighting for a huge list of languages. You use it like this:
:::python
import markdown
md = markdown.Markdown(extensions=['codehilite'])
foo = """
Beware the dreaded __trailing comma__.
Internet explorer will choke.
:::javascript
var foo = ['andrew', 'birds', 'bowl of', 'fire',];
console.log(foo);
"""
foo_html = md.convert(foo)
print(foo_html)
In this example, foo_html
will be html with the source code wrapped with <div class="codehilite"><pre>
. The syntax coloring is powered by span elements and css classes; you just need to link to a css file. Pygments has about 20 built-in styles (check out their demo page for examples). You can generate the css file for the friendly
style with this command:
:::bash
$ pygmentize -f html -S friendly -a .codehilite > mycssfile.css
With Flask/Jinja, using template filters is the most elegant way to integrate Markdown. Dan Colish as written a small jinja extension to enable it. Assuming his extension is the module flaskext
, then a simple Flask app with Markdown and code highlighting would be:
:::python
from flask import Flask, render_template
from flaskext.markdown import Markdown
import yaml
app = Flask(__name__)
md = Markdown(app, extensions = ['codehilite'] )
@app.route('/')
def index():
post = yaml.load(file('content/mypost.yaml', 'r'))
return render_template('mytemplate.html', post=post )
if __name__ == '__main__':
app.run()
If mypost.yaml
is:
:::yaml
title: 'A Dojo Object'
date: 2010-11-01
body: |
Let's do something in dojo
:::javascript
var b = dojo.byId('slideshow');
var mydiv = dojo.create('div', {'class':'foo'},b,'after'};
console.log("and that's about it");
Then you could render it in the template like so:
:::jinja
<html><body>
<h1>{{post.title}}</h1>
<p class='date'>{{post.date}}</p>
{{ post.body|markdown|safe }}
</body></html>
Flattening
My site is running on a basic apache+php setup. To publish the static site, I need to save each of the urls has a index.html
file. This is a straightforward programming exercise. They key is that flask has a test_client()
that takes a url as an argument and returns the html of the rendered page.
:::python
import shutil, os
# the output directory
FLATDIR = '/path/to/flatten/directory/'
# a list of all URLs on the site
routes = ['/','/about/','/blog/']
def flatten():
"""
Loops through urls in routes and creates an automatic
directory structure with index.htm files. When
uploaded to a standard apache file server, paths will resolve
with the same urls as in the local dev environment.
"""
# delete the whole flatten directory and recreate it
if os.path.exists(FLATDIR):
shutil.rmtree(FLATDIR)
os.makedirs(FLATDIR)
for p in routes:
# the output filesystem path cannot start with a /
# or os.path.join will assume it is the root of the volume
outdir = os.path.join(FLATDIR, p[1:] )
if not os.path.exists(outdir):
os.makedirs(outdir)
with app.test_client() as client:
resp = client.get(p)
if resp.status_code==200:
f = open(os.path.join(outdir, 'index.htm'), 'w')
f.write(resp.data)
f.close()
else:
msg = "Error {0} while pressing path {1}"
print(msg.format(resp.status_code, p))
Pushing out to the server
The final step is to push everything out to the server with Fabric. I’ll save that post for another day, but pushing it out to the server simply involves rsyncing the directory of flattened files and the directory of static content. The end result is a three step creation and publishing process.
:::bash
$ bbedit my/new/blog/post.yaml
$ hg -Am "added my new blog post"
$ fab press push