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.

Prerequisites

This example will require that you have both Django and PIL installed.

Getting Started

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: Create a deploy directory. Create a deploy/static directory. Copy all contents of static/ into deploy/static . Create a deploy/static/thumbnail directory. Create a deploy/static/image directory. For all images in images/ , create thumbnails and images in the deploy/static/thumbnail and deploy/static/image directories respectively. Render a list of templates we supply, and copy them into the 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.

Relative Versus Absolute Paths

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.

Expanding on the Idea: Thumbnail Gallery

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.

Expanding the Idea: Automated Builds

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.

Expanding the Idea: Rendering Markdown

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

Download the zipfile containing this project.

Let me know if there are any questions or comments!