September 15, 2008.
Update 9/15/2008 For anyone interested in the techniques discussed in this article, I put together AYM CMS which is a reusable template for building complex static websites using Django templates.
For a project I am working on I needed to create a simple website: some screenshots, some static pages, etc. The initial inclination is to just do static websites in static html and css, and that time honored solution has worked well for a couple of decades at this point, but I wanted something a bit better.
Recently I have been building increasing complex build scripts for my static websites, which inject rendered markdown into a static template, or generate their own thumbnails. This approach allows the benefits of static pages (insanely scalable on even the worst imaginable hardware), while negating most of the disadvantages of writing static html (too much human time consumed by updates and modifications).
Yesterday while staring down the prospect of copy-pasting a generic html template half a dozen times (and hoping I didn't need to make any modifications later) I thought to myself: "Damn it, I'll just use Django templates."
And what a sweet thought that was.
This example will require that you have both Django and PIL installed.
First, go ahead and create a folder to store this project in.
I am calling mine my_cms
.
mkdir my_cms
cd my_cms
Next we're going to create a handful of folders:
mkdir templates
mkdir static
mkdir deploy
mkdir images
Then we'll create a very simple settings.py
file in the
my_cms
directory.
import os, datetime
ROOT_PATH = os.path.dirname(__file__)
# setting up directory paths
TEMPLATE_DIRS = (
os.path.join(ROOT_PATH,'templates')
)
STATIC_DIR = os.path.join(ROOT_PATH,'static')
DEPLOY_DIR = os.path.join(ROOT_PATH,'deploy')
IMAGES_DIR = os.path.join(ROOT_PATH,'images')
# setting up some helpful values
STATIC_URL_FORMAT = u"/static/%s"
STATIC_THUMBNAIL_FORMAT = STATIC_URL_FORMAT % u"thumbnail/%s"
STATIC_IMAGE_FORMAT = STATIC_URL_FORMAT % u"image/%s"
THUMBNAIL_SIZE = (128,128)
EMAIL = u"your_email_address@gmail.com"
# creating default rendering context
CONTEXT = {
'email':EMAIL,
w':datetime.datetime.now(),
}
PAGES_TO_RENDER = ()
The settings stored in this settings.py
folder will be used
in both build script, and in particular the CONTEXT
dictionary
will be passed to templates as they are rendered, so it makes a
great place for settings and pieces of data that may change
frequently.
Now we'll write the initial version of our build script. Since we haven't created any content yet it'll be a bit plain, but here are the steps it will take:
deploy
directory.
deploy/static
directory.
static/
into deploy/static
.
deploy/static/thumbnail
directory.
deploy/static/image
directory.
images/
, create thumbnails and images
in the deploy/static/thumbnail
and deploy/static/image
directories respectively.
deploy/
directory.
Putting it all together, the build.py
script will look like this:
import os, shutil
from django.template.loader import render_to_string
from django.conf import settings
from PIL import Image
os.environ['DJANGO_SETTINGS_MODULE'] = u"settings"
def main():
# retrieving default context dictionary from settings
context = settings.CONTEXT
deploy_dir = settings.DEPLOY_DIR
print u"Removing existing deploy dir, if any..."
shutil.rmtree(deploy_dir,ignore_errors=True)
print u"Creating deploy/ dir..."
os.mkdir(deploy_dir)
print u"Copying contents of static/ into deploy/static..."
deploy_static_dir = os.path.join(deploy_dir,'static')
shutil.copytree(settings.STATIC_DIR,deploy_static_dir)
print u"Copying and creating thumbnails for files in images/..."
deploy_thumb_path = os.path.join(deploy_static_dir,'thumbnail')
deploy_image_path = os.path.join(deploy_static_dir,'image')
os.mkdir(deploy_thumb_path)
os.mkdir(deploy_image_path)
images = []
images_dict = {}
images_dir = settings.IMAGES_DIR
thumb_format = settings.STATIC_THUMBNAIL_FORMAT
image_format = settings.STATIC_IMAGE_FORMAT
thumbnail_dimensions = settings.THUMBNAIL_SIZE
for filename in os.listdir(images_dir):
# only process if ends with image file extension
before_ext,ext = os.path.splitext(filename)
if ext not in (".png",):
continue
print u"Copying and thumbnailing %s..." % filename
filepath = os.path.join(images_dir,filename)
im = Image.open(filepath)
im.save(os.path.join(deploy_image_path, filename),"PNG")
im.thumbnail(thumbnail_dimensions, Image.ANTIALIAS)
im.save(os.path.join(deploy_thumb_path, filename), "PNG")
# create dict with image data
image_dict = {}
image_dict['filename'] = filename
image_dict['thumbnail'] = thumb_format % filename
image_dict['image'] = image_format % filename
images.append(image_dict)
# before_ext is 'hello' in 'hello.png'
images_dict[before_ext] = image_dict
context['images'] = images
context['images_dict'] = images_dict
print u"Rendering pages..."
pages = settings.PAGES_TO_RENDER
for page in pages:
print u"Rendering %s..." % page
rendered = render_to_string(page, context)
page_path = os.path.join(deploy_dir, page)
fout = open(page_path,'w')
fout.write(rendered)
fout.close()
# completed build script
print u"Done running build.py."
if __name__ == "__main__":
main()
Admittedly its a bit on the long side for a snippet (76 lines of code?
yikes), but nothing there is terribly complex, and we won't have to
make any changes to it in the future (we'll just edit settings.py
instead).
Before we start writing out templates, lets throw a couple of random
images into the images/
folder so that we can test the thumbnail
creation parts. I used an old picture of mine whose
filename was bridge.png
. It doesn't matter too much what you use,
though (although the script is only setup to accept .png files, at
the moment).
A screenshot would be fine.
Now we just need to start writing our templates to take advantage
of this shiny new build system. First we'll write a base template,
with the filename base.html
in the templates/
directory.
<html>
<head>
<title>{% block title %}My CMS{% endblock %}</title>
<link rel="stylesheet" href="static/style.css">
</head>
<body>
{% include "header.html" %}
{% block content %}{% endblock %}
{% include "footer.html" %}
</body>
</html>
Then we'll create a header.html
file in templates/
.
<h1> Welcome to my Simple CMS </h1>
and a footer.html
file in templates/
.
<div class="footer">
<p>
Last updated at {{ now|time:"H:i" }}.
Mail <a href="mailto:{{ email }}">this address</a> with questions.
</p>
</div>
Admittedly we could have just as easily used the now
template tag instead of storing the now
variable in
our template's context. I did it that way just to give
an example of adding dynamic-static context to the
templates.
Finally, lets write the index.html
template.
{% extends "base.html" %}
{% block content %}
{% with images_dict.bridge as image %}
<a href="{{ image.image }}">
<img class="thumbnail" src="{{ image.thumbnail }}">
</a>
{% endwith %}
<p>
This is some simple static text.
</p>
<div class="thumbnails">
{% for image in images %}
<a href="{{ image.image }}">
<img class="thumbnail" src="{{ image.thumbnail }}">
</a>
{% endfor %}
</div>
{% endblock %}
Now to render the index.html
page we add it to the
PAGES_TO_RENDER
tuple in settings.py
.
PAGES_TO_RENDER = (
u"index.html",
)
and then go to the command line and type:
python build.py
And that will build the website in the deploy/
directory.
Finally, the base.html
template is already setup to
include the static/style.css
CSS file, so lets throw
together some really simple CSS as an example.
Create the my_cms/static/style.css
file.
body {
background-color: black;
color: white;
}
And then run the build script again:
python build.py
And the files in deploy/
will be displayed using the
new CSS file.
And with that, we're done.
Its worth noting that when loading the page off the local file system (as opposed to throwing it into the root directory of a webserver like Apache or nginx) the current path to the static directory won't work.
That is beacuse it is expecting the static folder at /static/
, which
happens to be your filesystem's root directory, not where you are serving
files from. If you keep your webpage urls flat (i.e. have pages like
/index.html, /help.html, /about.html, etc), then you can change the
STATIC_URL_FORMAT
setting in settings.py
to u"static/%s"
instead of u"/static/%s"
, and it will display correctly even when
not deploy in the root directory.
With the current version of this build script there is already some
helpfulness occuring. Each time you add an image to the image folder,
then you get a free thumbnail as well, and also get it added to both
the images
list and the images_dict
dictionary.
With those two tools you can display images by filename (minus the extension, because who wants to remember those?), or display their thumbnail instead.
You can create a screenshot gallery of all your site's images
(useful if you were creating a application showcase website,
for example) doing something as simple as dropping screenshots
into the images/
directory and adding this code to a template:
<div class="gallery">
{% for image in images %}
<div class="thumbnail">
<a href="{{ image.image }}">
<img src="{{ image.thumbnail }}">
</a>
</div>
{% endfor %}
</div>
For many webpages, that's all the dynamic functionality that you really need. And now that you're serving static files you're dinky little vps isn't going to commit suicide if there are more than ten concurrent visitors to your website.
For many websites, you're caching the pages for half an hour anyway, so for those thirty minutes your Django setup is essentially a mediocre file server.
Instead you can use a static solution like this script, and run a cron job that updates the static media every thirty minutes (or even everytime there is a file modified in or added to the directory).
Although this is certainly a poor solution for highly dynamic sites, for many sites such an approach may be sufficient.
One of the downsides of the simplest approach to this problem is that you'll be stuck writing in HTML instead of a more pleasant markup like Markdown or Textile.
But it doesn't have to be that way. Imagine a simple reworking of the Markdown filter to be used as a tag instead:
{% markdown %}
Render **all this!** to Markdown!
{% endmarkdown %}
It would take a couple of lines to implement, and would open up doors that are actually worth walking through.
You could even extend that idea to something like the syntax highlighting template filter, and have easy syntax highlighting in your static websites.
I'm sure you've already thought of more ideas of your own on how to improve upon this skeleton. There is a lot of low hanging fruit to making better static pages.
Download the zipfile containing this project.
Let me know if there are any questions or comments!