Intricate Static Websites With Django Templates
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 themy_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 theCONTEXT
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/
intodeploy/static
. - Create a
deploy/static/thumbnail
directory. - Create a
deploy/static/image
directory. - For all images in
images/
, create thumbnails and images in thedeploy/static/thumbnail
anddeploy/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
<span class="k">print</span> <span class="s">u"Removing existing deploy dir, if any..."</span> <span class="n">shutil</span><span class="o">.</span><span class="n">rmtree</span><span class="p">(</span><span class="n">deploy_dir</span><span class="p">,</span><span class="n">ignore_errors</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> <span class="k">print</span> <span class="s">u"Creating deploy/ dir..."</span> <span class="n">os</span><span class="o">.</span><span class="n">mkdir</span><span class="p">(</span><span class="n">deploy_dir</span><span class="p">)</span> <span class="k">print</span> <span class="s">u"Copying contents of static/ into deploy/static..."</span> <span class="n">deploy_static_dir</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">deploy_dir</span><span class="p">,</span><span class="s">'static'</span><span class="p">)</span> <span class="n">shutil</span><span class="o">.</span><span class="n">copytree</span><span class="p">(</span><span class="n">settings</span><span class="o">.</span><span class="n">STATIC_DIR</span><span class="p">,</span><span class="n">deploy_static_dir</span><span class="p">)</span> <span class="k">print</span> <span class="s">u"Copying and creating thumbnails for files in images/..."</span> <span class="n">deploy_thumb_path</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">deploy_static_dir</span><span class="p">,</span><span class="s">'thumbnail'</span><span class="p">)</span> <span class="n">deploy_image_path</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">deploy_static_dir</span><span class="p">,</span><span class="s">'image'</span><span class="p">)</span> <span class="n">os</span><span class="o">.</span><span class="n">mkdir</span><span class="p">(</span><span class="n">deploy_thumb_path</span><span class="p">)</span> <span class="n">os</span><span class="o">.</span><span class="n">mkdir</span><span class="p">(</span><span class="n">deploy_image_path</span><span class="p">)</span> <span class="n">images</span> <span class="o">=</span> <span class="p">[]</span> <span class="n">images_dict</span> <span class="o">=</span> <span class="p">{}</span> <span class="n">images_dir</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">IMAGES_DIR</span> <span class="n">thumb_format</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">STATIC_THUMBNAIL_FORMAT</span> <span class="n">image_format</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">STATIC_IMAGE_FORMAT</span> <span class="n">thumbnail_dimensions</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">THUMBNAIL_SIZE</span> <span class="k">for</span> <span class="n">filename</span> <span class="ow">in</span> <span class="n">os</span><span class="o">.</span><span class="n">listdir</span><span class="p">(</span><span class="n">images_dir</span><span class="p">):</span> <span class="c"># only process if ends with image file extension</span> <span class="n">before_ext</span><span class="p">,</span><span class="n">ext</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">splitext</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span> <span class="k">if</span> <span class="n">ext</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">(</span><span class="s">".png"</span><span class="p">,):</span> <span class="k">continue</span> <span class="k">print</span> <span class="s">u"Copying and thumbnailing </span><span class="si">%s</span><span class="s">..."</span> <span class="o">%</span> <span class="n">filename</span> <span class="n">filepath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">images_dir</span><span class="p">,</span><span class="n">filename</span><span class="p">)</span> <span class="n">im</span> <span class="o">=</span> <span class="n">Image</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="n">filepath</span><span class="p">)</span> <span class="n">im</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">deploy_image_path</span><span class="p">,</span> <span class="n">filename</span><span class="p">),</span><span class="s">"PNG"</span><span class="p">)</span> <span class="n">im</span><span class="o">.</span><span class="n">thumbnail</span><span class="p">(</span><span class="n">thumbnail_dimensions</span><span class="p">,</span> <span class="n">Image</span><span class="o">.</span><span class="n">ANTIALIAS</span><span class="p">)</span> <span class="n">im</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">deploy_thumb_path</span><span class="p">,</span> <span class="n">filename</span><span class="p">),</span> <span class="s">"PNG"</span><span class="p">)</span> <span class="c"># create dict with image data </span> <span class="n">image_dict</span> <span class="o">=</span> <span class="p">{}</span> <span class="n">image_dict</span><span class="p">[</span><span class="s">'filename'</span><span class="p">]</span> <span class="o">=</span> <span class="n">filename</span> <span class="n">image_dict</span><span class="p">[</span><span class="s">'thumbnail'</span><span class="p">]</span> <span class="o">=</span> <span class="n">thumb_format</span> <span class="o">%</span> <span class="n">filename</span> <span class="n">image_dict</span><span class="p">[</span><span class="s">'image'</span><span class="p">]</span> <span class="o">=</span> <span class="n">image_format</span> <span class="o">%</span> <span class="n">filename</span> <span class="n">images</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">image_dict</span><span class="p">)</span> <span class="c"># before_ext is 'hello' in 'hello.png'</span> <span class="n">images_dict</span><span class="p">[</span><span class="n">before_ext</span><span class="p">]</span> <span class="o">=</span> <span class="n">image_dict</span> <span class="n">context</span><span class="p">[</span><span class="s">'images'</span><span class="p">]</span> <span class="o">=</span> <span class="n">images</span> <span class="n">context</span><span class="p">[</span><span class="s">'images_dict'</span><span class="p">]</span> <span class="o">=</span> <span class="n">images_dict</span> <span class="k">print</span> <span class="s">u"Rendering pages..."</span> <span class="n">pages</span> <span class="o">=</span> <span class="n">settings</span><span class="o">.</span><span class="n">PAGES_TO_RENDER</span> <span class="k">for</span> <span class="n">page</span> <span class="ow">in</span> <span class="n">pages</span><span class="p">:</span> <span class="k">print</span> <span class="s">u"Rendering </span><span class="si">%s</span><span class="s">..."</span> <span class="o">%</span> <span class="n">page</span> <span class="n">rendered</span> <span class="o">=</span> <span class="n">render_to_string</span><span class="p">(</span><span class="n">page</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span> <span class="n">page_path</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">deploy_dir</span><span class="p">,</span> <span class="n">page</span><span class="p">)</span> <span class="n">fout</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="n">page_path</span><span class="p">,</span><span class="s">'w'</span><span class="p">)</span> <span class="n">fout</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span> <span class="n">fout</span><span class="o">.</span><span class="n">close</span><span class="p">()</span> <span class="c"># completed build script</span> <span class="k">print</span> <span class="s">u"Done running build.py."</span>
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).- 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.html
in 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.html
file intemplates/
.<h1> Welcome to my Simple CMS </h1>
and a
footer.html
file 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
now
template tag instead of storing thenow
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 thePAGES_TO_RENDER
tuple 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.html
template is already setup to include thestatic/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!