Create simple websites fast, now.

Poole is an easy to use Markdown driven static website generator. You write the content of your pages in Markdown and Poole creates a nice and simple site with a navigation menu. You don't need to learn a template or preprocessing engine.

Though Poole is made for simple sites, it has basic support for content generation by embedding Python code in page source files. This is a dirty merge of content and logic but for simple sites it's a pragmatic way to get things done fast and easy -- if you know Python you're ready to start.

See what you get: example site.

Flattr this


You should know Markdown and optionally Python if you want to use Poole's dirty content generation capability.

Your system should have installed Python ≥ 2.5 and python-markdown. Poole has been tested on Linux but should also work on other Unix systems and Windows (in theory, report an issue if it fails).

Getting Started

Clone oder download (zip, tgz) the repository and then put to your PATH:

$ hg clone /some/where/poole
$ export PATH=$PATH:/some/where/poole

TIP: You might want to add the last command to your ~/.bashrc.

Create and build a site project:

$ mkdir /path/to/site/project
$ cd /path/to/site/project
$ --init
$ --build
$ --serve

Done. You've just created a website! Browse http://localhost:8080/ and watch the example pages which have been created during initialization.

To write your own pages, use the example pages in the input folder as a starting point.

Run --build whenever you've made some changes in the input folder.

How It Works

Poole takes files from a project's input directory and copies them to the output directory. In this process files ending with md, mkd, mdown or markdown get converted to HTML using the project's page.html as a skeleton.

Additionally Poole expands any macros used in a page. Don't care about that for now ..

When running --build in a Poole project, an input directory like this:

|- input
    |- news.mkd
    |- foo.mdown
    |- images
        |- bar.png

will result in an output folder like that:

|- output
    |- index.html
    |- news.html
    |- foo.html
    |- images
        |- bar.png

Page Layout

Every Poole page is based on the skeleton file page.html. Hence adjusting the site layout means adjusting page.html and extending or replacing its CCS file input/poole.css.

The only thing you should keep in page.html are the embedded {{__content__}} and {{__encoding__}} expressions. Below is an almost minimal page.html file. It does not look nice but it's a clean starting point to build your own layout from scratch.

Minimal page.html:

    <meta http-equiv="Content-Type" content="text/html; charset={{ __encoding__ }}" />
    {{ __content __ }}

It's easy to apply one of the numerous free CSS templates out there to a Poole site. For more information read this blog post with step-by-step instructions.

Content Generation

Poole allows you to embed Python code in your pages to generate content:


Here is normal text in *markdown* flavor.
print "hello poole"
Did you know? The sum of 2 and 2 is {{ 2 + 2 }}.

This example demonstrates 2 ways to embed Python code, either as statements or as expressions:

  1. Everything between {% and %} are statements and whatever is printed to stdout during their execution is going to be part of the final HTML page.
  2. Everything between {{ and }} are expressions and their evaluation is going to be part of the final page.

TIP: Instead of the outer curly brackets { and } you can also use <!-- and --> to prevent syntax highlighting markdown editors from getting confused by the Python code.

TIP: To print the code surrounding tags literally, simply escape the opening tag with a backslash.

Outsource complex or frequently used code

To keep embedded code short and compact or to reuse it in several pages, it can be outsourced into a file called in a project's root folder (where the page.html file is located). Every public attribute in is available within embedded Python code blocks:

from datetime import date
def today():
    return"%B %d, %Y")

author = "Popeye"


This site has been updated on {{ today() }} by {{ author }}.

Working with pages

Next to stuff defined in the objects page and pages are available in embedded Python code. The first one is a dictionary describing the page in which the code is embedded. The second one is a list of all pages in the project.

The following attributes are always set in a page dictionary:

  • title: The page's title, by default its filename without extension (setting alternatives is described in the next section).
  • fname: Path to the page's source file, e.g. /path/to/project/input/stuff/
  • url: The page's relative URL, e.g. for a source page input/stuff/ this is stuff/news.html.

The example page.html file in a freshly initialized site project uses a page's title attribute:

<div id="header">
     <h1>a poole site</h1>
     <h2>{{ page["title"] }}</h2>

TIP: All items in a page dictionary are exposed as attributes, i.e. page["foobar"] is identical to page.foobar. Dictionary access is useful if an item may not be set, e.g.: page.get("foobar", "...").

Setting page attributes

Page attributes can be set at the top of a page's source file, in Python's configuration file style. They are delimited from the page's content by a line with 3 or more dashes.


title: Hot News
foobar: King Kong
Here are some news about {{ page.foobar }}.
Did I say {% print(page.foobar) %}?

That way you can also set a page's title explicitly, instead of using the file name. Other useful attributes to set are description and keywords, which get used by the default page.html file to set HTML meta tags. Here it comes in handy to set default page attributes in the file:

page = { "description": "some stuff", "keywords": "stuff" }

That way you can safely use the description and keywords attributes without bothering if they are really defined in every page.

Accessing page objects in the macros module

The objects pages and page are also available within That means you can define them as dummys and reference them in Poole updates them when loading the macros module.

page = {} # you can also set defaults here, see previous section
pages = []

def something():
    # when executing this, the page and pages objects above are up-to-date
    print page["title"]

Options and paths

Similarly to page and pages the following objects are available within embedded Python code and within the macros module:

  • options: The command line arguments given to Poole as parsed by Python's optparse module. For instance the base URL can be retrieved by options.base_url.
  • project: Path to the project's root directory.
  • input: Path to the project's input directory.
  • output: Path to the project's output directory.

Character encodings

In case you use non-ASCII characters, check the encoding options of Poole. In most cases working with non-ASCII strings should work straight forward if the options are set properly (default is UTF-8).

However, be aware that page variables defined within page source files and derived from a page's file name internally are handled as Python unicode objects. That means if you want to refer to non-ASCII page variable names and values form within embedded Python code or from, make sure to use unicode strings to reference them.


Navigation menu

Have a look into the page.html file in a freshly initialized Poole project.

List of blog posts

If you want to write some blog posts, you probably would like to have a page listing all or the latest blog posts. This is easy if you set certain page attributes in every blog post page:


title: blog
post: This is your brain on mongs
date: 2010-03-01

# {{ }}

Posted on {{ }}

My hero is full of keyboards. Get nonsense at <>


This is my blog.

# My posts

from datetime import datetime
posts = [p for p in pages if "post" in p] # get all blog post pages
posts.sort(key=lambda p: p.get("date"), reverse=True) # sort post pages by date
for p in posts:
    date = datetime.strptime(p["date"], "%Y-%m-%d").strftime("%B %d, %Y")
    print "  * **[%s](%s)** - %s" % (, p.url, date) # markdown list item

Feel free to adjust this to your needs.

TIP: Instead of setting the post title and date as page attributes, you can encode them in the page's file name using a structure like For instance for the file name Poole would automatically set the page attributes which has been set manually in the example above.

To see this example in action, have a look into the example pages in a freshly initialized Poole project.

Google sitemap file

To generate a Google sitemap.xml file, put this into the project's file:

# -----------------------------------------------------------------------------
# generate sitemap.xml
# -----------------------------------------------------------------------------

from datetime import datetime
import os.path

_SITEMAP = """<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="">


def hook_preconvert_sitemap():
    """Generate Google sitemap.xml file."""
    date = datetime.strftime(, "%Y-%m-%d")
    urls = []
    for p in pages:
        urls.append(_SITEMAP_URL % (options.base_url.rstrip('/'), p.url, date,
                    p.get("changefreq", "monthly"), p.get("priority", "0.8")))
    fname = os.path.join(options.project, "output", "sitemap.xml")
    fp = open(fname, 'w')
    fp.write(_SITEMAP % "".join(urls))

You probably want to adjust the default values for changefreq and priority.

Info: Every function in whose name starts with hook_preconvert_ or hook_postconvert_ is executed exactly once per project build -- either before or after converting pages from markdown to HTML. In post-convert hooks the HTML content of a page (yet without header and footer) can be accessed with page.html. This is useful to generate full-content RSS feeds.

RSS feed for blog posts

To generate an RSS feed for blog posts put this into the project's file and adjust for your site:

# -----------------------------------------------------------------------------
# generate rss feed
# -----------------------------------------------------------------------------

import email.utils
import os.path
import time

_RSS = """<?xml version="1.0"?>
<rss version="2.0">

_RSS_ITEM = """

def hook_postconvert_rss():
    items = []
    posts = [p for p in pages if "post" in p] # get all blog post pages
    posts.sort(key=lambda p:, reverse=True)
    for p in posts:
        title =
        link = "%s/%s" % (options.base_url.rstrip("/"), p.url)
        desc = p.get("description", "")
        date = time.mktime(time.strptime("%s 12" %, "%Y-%m-%d %H"))
        date = email.utils.formatdate(date)
        items.append(_RSS_ITEM % (title, link, desc, date, link))

    items = "".join(items)

    # --- CHANGE THIS --- #
    title = "Maximum volume yields maximum moustaches"
    link = "%s/blog.html" % options.base_url.rstrip("/")
    desc = "My name is dragonforce. You killed my dragons. Prepare to scream."
    date = email.utils.formatdate()

    rss = _RSS % (title, link, desc, date, date, items)

    fp = open(os.path.join(output, "rss.xml"), 'w')

Convert CleverCSS to CSS

To convert all CleverCSS files in the input directory to regular CSS files in the output directory, put this into the project's (assuming you use .ccss as file name extension):

# -----------------------------------------------------------------------------
# convert clevercss files to css
# -----------------------------------------------------------------------------

import clevercss
import glob
import os.path

def hook_preconvert_ccss(): # pre- or post-convert hook, doesn't matter
    for ccss in glob.glob(os.path.join(input, "**.ccss")):
        css = ccss[len(input):].lstrip("/")
        css = "%s.css" % os.path.splitext(css)[0]
        css = os.path.join(output, css)
        fpi = open(ccss)
        fpo = open(css, 'w')

To prevent the original CCSS files from getting copied to the output directory, use Poole's --ignore option: --build --ignore '^\.|~$|\.ccss$'.


Please use the issue tracker.