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!