Intricate Static Websites With Django Templates

09/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.

Prerequisites

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

Getting Started

  1. First, go ahead and create a folder to store this project in. I am calling mine my_cms.

    mkdir my_cms
    cd my_cms
    
  2. Next we're going to create a handful of folders:

    mkdir templates
    mkdir static
    mkdir deploy
    mkdir images
    
  3. 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.

  4. 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:

    1. Create a deploy directory.
    2. Create a deploy/static directory.
    3. Copy all contents of static/ into deploy/static.
    4. Create a deploy/static/thumbnail directory.
    5. Create a deploy/static/image directory.
    6. For all images in images/, create thumbnails and images in the deploy/static/thumbnail and deploy/static/image directories respectively.
    7. 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).

  5. 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.

  6. 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 %}
    
  7. 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.

  8. 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!

All Rights Reserved, Will Larson 2007 - 2014.