Ampoule Repository

Ampoule is a lightweight, simple yet flexible, static site generator written in Python. It uses Jinja2 for templating.

Features

Minimal example

import string
from datetime import datetime
import string

import ampoule_ssg as ampoule
from ampoule_ssg import markdown

# Create a site object. This is where we are adding pages to. The argument is the directory
# where the site will be built.
site = ampoule.Site("my_site")


# Use this as "| markdown" in Jinja2 templates to convert any Markdown source to HTML.
@site.filter("markdown")
def markdown_filter(text):
   return markdown.markdown2html(text)


# Make the URLs web-friendly and make it end in ".html" so it will be correctly formatted
# by dumb servers.
def article_url(url):
   url = url.lower().rpartition(".")[0]

   new_url = ""
   for i in url:
       if i in string.ascii_lowercase:
           new_url += i
       elif i in string.whitespace:
           new_url += "-"

   return new_url + ".html"


# Set context that will be passed to all templates. You can still override this.
site.context["timestamp"] = datetime.now()
site.context["ampoule"] = ampoule

# Add the index of articles. In the template, we're looping over it to list them all.
articles = ampoule.Index("articles", url_transform=article_url, sort_by=lambda x: x.date)
# This makes it take all indexed files and put them under the /articles URL, keeping the
# index's URL transformation and placing all of them in the article.html template. This
# will be passed as "document" to the template.
site.add_from_index(articles, "/articles", "article.html")

# Create the main page which has access to the index so it can list all articles.
main_page = ampoule.Page(site, "home.html", articles=articles)

# Add the page. Note how we're binding it to a path; it will automatically be set as
# index.html in that directory, and the URL is site-relative, not the OS root.
site.add_page("/", main_page)

# Add static files using a recursive static index. It will add all files in the static
# directory and all its subdirectories, without putting them into templates. You could
# still use them in templates, so you can make a photo gallery or something.
site.add_from_index(
       # We're excluding Markdown files because we're using them as licence information
       # for when the site is distributed together with the fonts. You can exclude any
       # file you want using regex.
       ampoule.Index("static", recursive=True, exclude=r"\.md$"),
       "/static",
       # There is no template, because the index is static.
       static=True
)

# Makes Ampoule take all pages and put them in a directory.
site.build()

More information

Name origin

An ampoule is smaller than a flask. Because it is related to Flask (it uses Jinja2) but is a much smaller static version of it, the name makes sense.

Why not use Jekyll, Hugo, or Pelican?

Jekyll

Jekyll is Ruby, and it's very much Ruby, in fact, it's 17,000 lines of Ruby. Configuration is done only in YAML. It's hard to extend, and the plugin and theme systems are overengineered. It's also limited (no OOP, not even multiple index directories). And for the Flask users like me, it uses a template language that is very similar to Jinja2, but it's not Jinja2, so templates aren't reusable. I've actually used Jekyll, and it's not nice.

Hugo

Hugo is actually pretty interesting, but it's not as flexible, because it's not scriptable, and a compiled language is not appropriate for this. Did I mention it also has an overengineered theme system? And it's even larger, a whooping 133,000 lines of Go! You can never know it all.

Pelican

Pelican is opinionated and relies on plugins for everything. It's extremely limited, in fact it can only do blogs. It also can't be portable and implemented in Flask.

Why even use a static site generator?

You've got two other options. Let's examine them.

Dynamic sites

Static sites

With a generated static site, you get the best of both worlds. It's the best publishing platform, because it's just files.

How to install

Please note that this is not yet available on PyPI. For now you'll need to download the code (ideally using git) and install it with pip as a local package by giving it the path to the directory containing setup.py.

Full documentation

To demonstrate just how easy it is, the docs can all fit on one page.

class ampoule_ssg.Site

Site is the main class of Ampoule; it represents a single website. It is responsible for handling added pages, the template engine and features, as well as building it.

def __init__(self, build_dir: typing.Union[str, bytes, os.PathLike], template_dir: typing.Union[str, bytes, os.PathLike] = "templates")

Create a new site object. build_dir is the directory where the site will be built. template_dir is the directory where the templates are stored. Both are relative to the script current working directory.

def add_page(self, location: typing.Union[str, bytes, os.PathLike], page: typing.union[Static, Page])

Add a page object to the site at the server-relative URL location. The page object can be either a Static or a Page.

def add_from_index(self, index: Index, location: typing.Union[str, bytes, os.PathLike], template: str = None, **kwargs)

Add all pages from an index to the site with the root at the server-relative URL location. The pages will be rendered with the template template and the context kwargs. will be passed to all of them. If the index is static, the pages will not be rendered with a template, but rather copied as-is.

For each page, the document object found in the index will be passed to the template under that name.

def filter(self, name: str)

A decorator that registers a filter function with the site. The function should take at least one argument, the value to be filtered, and return the filtered value.

def test(self, name: str)

A decorator that registers a test function with the site. The function should take at least one argument, the value to be tested, and return a boolean.

def build(self)

Build (save) the site to the build directory it was constructed with. This will create the directory if it does not exist, clear it (but not delete it) and then write all the pages.

context: dict[str, typing.Any]

A dictionary containing names that are available to all pages. It can be overriden by the page's context or modified at any time.

class ampoule_ssg.Page(str)

Page is a class that represents a single page on the site. A page is composed of a template, a document and a context.

def __new__(cls, site: Site, template: str, document: Document = None, **kwargs)

Create a new page object. site is the site object that the page belongs to. template is the template the document will be put in. document is the document object that will be passed to the template. kwargs are names that will be available to the template for additional context.

If there's no document, it will not be available to the template. This is useful for single pages with fully static content, like a contact page.

class ampoule_ssg.Static(bytes)

Static is a class that represents a single static file on the site. A static file is just the content, in binary format, and it doesn't use templating.

def __new__(cls, site: Site, document: Document)

Create a new static object. site is the site object that the static file belongs to. document is the document object that will be written to the file; it can contain any encoding, even text, and will be written as-is.

class ampoule_ssg.Index

An index is a collection of documents that can be iterated over or added to a site using a common template (see ampoule_ssg.Site.add_from_index).

def __init__(self, directory: typing.Union[str, bytes, os.PathLike], recursive: bool = False, url_transform: typing.Callable = lambda x: x, sort_by: typing.Callable = lambda x: x.file_name, exclude: typing.Union[str, NoneType] = None, static: bool = False)

Create a new index. directory is the directory to get content from. If recursive is true, the whole tree of that directory will be indexed. url_transform is a function that will be applied to the file name to get the new file name. Generally you want to set it so it makes them end in .html so dumb servers can serve them correctly. However, for static files you most likely will not set it. sort_by is the key after which to sort the documents after they are indexed; by default it is the file name. exclude is a regular expression that will be used to exclude files from the index. If the index is static, all documents will be parsed as-is, without removing front matter.

def __iter__(self)

Return an iterator for the index.

def __next__(self)

Get the next document in the index.

def __repr__(self)

Return a string representation of the index. It contains the directory and the names of the documents in it.

def __len__(self)

Return the number of documents in the index, that is, its length.

class ampoule_ssg.Document

A document is a file, not rendered, but available for use. It is what is passed to the template as document for processing. Generally, you won't create these yourself, but rather use them as they are returned by an index. However, if you do need one, you can create it manually and pass it to a page.

Documents will parse YAML front matter for textual files, unless disabled. The front matter is available as an attribute of the document, and can be accessed using indexing syntax.

def __init__(self, file_name: typing.Union[str, bytes, os.PathLike], url_transform: typing.Callable = lambda x: x, front_matter_enabled: bool = True)

Create a new document. file_name is the name of the file. url_transform is a function that will be applied to the file name to get the new file name; it has the same meaning as in the Index. front_matter_enabled is a boolean that determines whether the document will parse YAML front matter.

def __repr__(self)

Return a string containing Document and the file name.

def __getitem__(self, item: str)

Access the document's front matter. If front matter is disabled or not available, this will never work.

def __setitem__(self, item: str, value: typing.Any)

Change the document's front matter. It works even if it wasn't parsed, because YAML behaves like a dictionary.

def __delitem__(self, item: str)

Delete an item from the document's front matter.

def __contains__(self, item: str)

Check if an item is in the document's front matter.