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_cmsNext 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.pyfile in themy_cmsdirectory.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.pyfolder will be used in both build script, and in particular theCONTEXTdictionary 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
deploydirectory. -
Create a
deploy/staticdirectory. -
Copy all contents of
static/intodeploy/static. -
Create a
deploy/static/thumbnaildirectory. -
Create a
deploy/static/imagedirectory. -
For all images in
images/, create thumbnails and images in thedeploy/static/thumbnailanddeploy/static/imagedirectories respectively. -
Render a list of templates we supply, and copy them into
the
deploy/directory.
Putting it all together, the
build.pyscript 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.pyinstead).-
Create a
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 wasbridge.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.htmlin thetemplates/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.htmlfile intemplates/.<h1> Welcome to my Simple CMS </h1>
and a
footer.htmlfile intemplates/.<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
nowtemplate tag instead of storing thenowvariable 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.htmltemplate.{% 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.htmlpage we add it to thePAGES_TO_RENDERtuple insettings.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.htmltemplate is already setup to include thestatic/style.cssCSS file, so lets throw together some really simple CSS as an example.Create the
my_cms/static/style.cssfile.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 django filters can be used as tags with the 'filter' tag, e.g.:
This looks like a good approach; I like the idea of being able to use django's templating with needing a full-blown project.
Ahh, yes, I completely forgot about the filter template tag. Thanks for reminding me. If only you'd been there a few hours earlier while I was writing it...
I think the approach has promise, essentially adding a potent pre-processing layer to static (or even PHP or what not) projects. And it doesn't require anything special on your deployment server, which is a small blessing for people working on shared servers or who aren't latent system administrators.
I implemented a similiar generator ( Codeboje website builder ) some time ago. In contrast to your solution i don't use a project file e.g. your settings.py and work on a folder structure and name based schema. Textblock are in markdown and reside in their own files, not in the template.
Currently i am thinking about how to handle metadata of media assets. Do you have any plans for that?
I like to work?
This is very nice. I've been using a tool called Tahchee that expands Cheetah templates into a static website.
It has two modes - local / remote that handle the problem of FILE:// vs. HTTP:// and linking.
If I get into Django templating more than I am, I'll be giving your code a go.
Anyway thanks for posting this and all of the other great articles. I probably added 10 bookmarks this morning to your site.