The most dynamic static site you'll ever see

Oct 27, 2018 | Python , Programming

I'm not completely convinced by the current landscape of static site generators. Most of these don't generate "sites". They generate "blogs", and the concept of "pages" and "posts" is so deeply ingrained in the implementation that it's difficult to break free of that structure. Eventually this starts showing up in your static "site".

The point is, I'm missing flexibility.

I started working on Developer to Manager recently, and right from the beginning on I wanted it to be a static site (which it is). That being said, I still had some pretty dynamic features planned for it so I wanted a tool that could help me do that.

MVC enabled static sites

Luckily I found statik which solved most of my problems. The idea behind statik is pretty neat and as far as I understand, it directly maps to the MVC pattern.

The models are defined using YAML files and the actual site data is written in Markdown.

You then define "views", which in my mind map to MVC controllers. These are YAML files specifying page metadata - things like the models that this page is supposed to render, which template to pick, which URL to render at, and so on.

Finally, you define templates which are standard Jinja files and are rendered using the views config.

This setup worked fairly well, until I started running into special cases. At that point I tried finding alternatives which would let me have more flexibility than what statik offered and still let me have a static site at the end.

Freezing Flask applications

Even though I couldn't find a convincing alternative, what I did find was Frozen-Flask. The idea is that it lets you "freeze" your Flask application to a set of static HTML files. So you basically build a normal web app and then ask Frozen-Flask to compile the site into plain HTML which you can deploy anywhere you like.

This setup is really nice, but with only one disadvantage - defining your site data is still not convenient. You have to go through the hassle of setting up a data store (like SQLite or PostgreSQL). And while these data stores do the job extremely well, it's more or less overkill for the purpose of a static website.

Besides, depending on how many data models you have and what types they contain, this can quickly get out of hand. Imagine writing an INSERT statement for a blog post. In addition, you can't version control your data. I mean, technically you can, but the diffs won't make any sense to a human.

The only missing link here seemed to be the ability to define data models and enter data like a human. So I spent a weekend working on this, and Flask-FileAlchemy is what came out.

Flask-FileAlchemy

Flask-FileAlchemy is a Python package that lets you define your static data in plain text files using a combination of YAML and Markdown.

Why plain text files? Because plain text has the advantage that it's much easier to handle for a human. And there's the added advantage that you can check these files into source control so that your application code and the data both have a common history.

So how does it work?

You start with a normal Flask app and define your data models using Flask-SQLAlchemy. You then add a directory somewhere on disk which acts like your data "storage" and contains a subdirectory for each model you've defined. Flask-FileAlchemy then loads all the data from these subdirectories, puts them into whatever data store you're using (in-memory SQLite is recommended), and makes it all available for your app to query via the standard Flask-SQLAlchemy session interface.

This lets you retain the comfort of dynamic sites without compromising on the simplicity of static files.

Walkthrough

Let's walk through a simple example to show how it really works.

Step 1, define your app and the associated SQLAlchemy models.

app = Flask(__name__)

# configure Flask-SQLAlchemy
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'

db = SQLAlchemy(app)

class BlogPost(db.Model):
   __tablename__ = 'blog_posts'

   slug = Column(String(255), primary_key=True)
   title = Column(String(255), nullable=False)
   contents = Column(Text, nullable=False)

After this, create a data/ directory somewhere on your disk. Under this directory, create a blog_posts directory for storing all the blog posts. In general, this data/ directory is supposed to contain one directory per model, with the same name as the model's __tablename__ attribute.

In this example, we'll create a single blog post by entering the following contents in the data/blog_posts/first-post-ever.yml file.

slug: first-post-ever
title: First post ever!
contents: |
  This blog post talks about how it's the first post ever!

Finally, configure Flask-FileAlchemy with this setup and ask it to load all your data.

# configure Flask-FileAlchemy
app.config['FILEALCHEMY_DATA_DIR'] = os.path.join(
   os.path.dirname(os.path.realpath(__file__)), 'data'
)
app.config['FILEALCHEMY_MODELS'] = (BlogPost,)

# load tables
FileAlchemy(app, db).load_tables()

Flask-FileAlchemy then loads all the data under that directory and stores it in the data store of your choice that you configured (again, the preference being in-memory SQLite). You can then use db.session to query all this data like you would in any normal application.

At this point, you would have a fully functioning Flask app running locally, on which you can just run Frozen-Flask to get a bunch of HTML files.

Conclusion

I'm obviously biased, but I do think the result is quite neat and useful. I don't see myself switching away from the existing static site generators (like Pelican) any time soon for general purpose blogs. Mostly because at this point a lot of people have contributed a lot of effort into building and maintaining these packages and it's hard to beat that quality, let alone gaining feature parity.

At the same time, for sites that are slightly more complicated or require a bit more flexibility than what a blog generator would give you, I definitely see myself using Flask-FileAlchemy in the future. Developer to Manager is already running using this stack, and I think that this package is useful enough to build more non-trivial static sites.

I've released the module on PyPI under the name flask-filealchemy. Hope it's useful!