Welcome to Exhibition’s documentation!

Say it right:
/ɛgs’hɪb’ɪʃ(ə)n/
So something like:
eggs hib ish’n
What?
A static site generator
Where?
License?
GPLv3 or later. See LICENSE for the actual text.
Why though?
I’d been using Hyde for a number of years, eventually that project stopped receiving updates. Hyde had very limited test coverage, many features that I didn’t personally use, and no Python 3 support. This combination made the prospect of maintaining Hyde daunting, so forking was out of the question.
In the end, I wrote Exhibition as other available static site generators would either require massively rewriting the sites I already had or weren’t flexible enough to generate the same URL structure.
What’s the status of this project?
There are tests, there’s some documentation, and I currently use it for a number of websites, including my personal blog.
Please feel free to add your site to the wiki if it uses Exhibition, but please make sure its safe for work and not covered in adverts.
Contributions
I’m always looking for contributions, whether they be bug reports, bug fixes, feature requests, new features, or documentation. Also, feel free to open issues for support requests too - these are very helpful in showing me where documentation is required or needs improving.
Code Of Conduct
The Exhibition project has adopted the Contributor Covenant Code version 1.4. By contributing to this project, you agree to abide by its terms.
The full text of the code of conduct can be found here
Getting started
Exhibition is fairly quick to configure.
Minimum setup
At minimum, Exhibition expects to find a YAML file, site.yaml
, with at
least deploy_path
and content_path
defined. The path specified in
content_path
needs to exist.
For example:
$ mkdir content
$ cat << EOF > site.yaml
> deploy_path: deploy
> content_path: content
> EOF
Note
Exhibition comes with a starter template for new sites. See exhibit create --help
for more information.
You can now generate your first Exhibition website!:
$ exhibit gen
$ ls deploy
Of course, you’ve got no content so the directory will be empty.
Any file or directory you put in content
will appear in deploy
when you
run exhibit gen
.
Templates
Exhibition supports Jinja2 out of the box, but it needs to be enabled:
deploy_path: deploy
content_path: content
filter: exhibition.filters.jinja2
templates: templates
Now we can create HTML files that use Jinja2 template syntax:
<html>
<body>
<p>This page has {{ node.siblings|length }} siblings</p>
</body>
</html>
Note
node
is the current page being rendered and is passed to Jinja2 as a context variable.
Run exhibit gen
and then exhibit serve
. If you connect to
http://localhost:8000
you’ll see the following text:
This page has 0 siblings
If you add another page, this number will increase when run exhibit gen
again.
If you wish to use template inheritance, create a directory named “templates”
and add your templates to it. You can either use the extends tag directly or
you can specify extends
in site.yaml
. You can also specify
default_block
to save you from wrapping every page in
{% block content %}
:
extends: page.j2
default_block: content
And then our template:
<html>
<body>
{% block content %}{% endblock %}
</body>
</html>
Our index page would be this:
<p>This page has {{ node.siblings|length }} siblings</p>
The generated HTML will be exactly the same, except now files in content/
will not have to each have their own copy of any headings, page title, links to
CSS or whatever.
Meta
Site settings are available in templates as node.meta
. For example:
<p>Current filter is "{{ node.meta.filter }}"</p>
Which will generate the following:
Current filter is "exhibition.filters.jinja2"
You can reference any data that you put in site.yaml
like this - and
there’s no limit on what you can put in there.
As well as site.yaml
there are two additional places that settings can be
controlled: meta.yaml
and frontmatter.
Meta files
A meta.yaml
can be used to define or override settings for a particular
directory and any files or subdirectories it contains.
Let’s add a blog to our website:
$ mkdir content/blog
$ cat << EOF > content/blog/meta.yaml
> extends: blog_post.j2
Now all HTML files in content/blog/
will use the blog_post.j2
as their
base template rather than page.j2
, but files such as content/index.html
will still use page.j2
as their base template.
Note
meta.yaml
files do not appear as nodes and won’t appear in deploy_path
Frontmatter
Frontmatter is the term used to describe YAML metadata put at the beginning of
a file. Unlike meta.yaml
, any settings defined (or overridden) here will
only affect this one file.
For example, we won’t want the index page of our blog to use blog_post.j2
as its base template:
---
extends: blog_index.j2
---
{% for post in node.sibling %}
<p><a href="{{ post.full_url }}">{{ post.meta.title }}</a></p>
---
title: My First Post
---
<h1>{{ node.meta.title }}
<p>Hey! This is my first blog post!</p>
What next?
Checkout the API. File bugs. Submit patches.
Exhibition is still in the early stages of development, so please contribute!
exhibit
commandline script
exhibit
A Python static site generator
exhibit [OPTIONS] COMMAND [ARGS]...
Options
- --version
Show the version and exit.
- -v, --verbose
Verbose output, can be used multiple times to increase logging level
create
Generate a starter site with a basic configuration for website.
exhibit create [OPTIONS] PATH
Options
- -f, --force
overwrite destination if it exists
Arguments
- PATH
Required argument
gen
Generate site from content_path
exhibit gen [OPTIONS]
serve
Serve files from deploy_path as a webserver would
exhibit serve [OPTIONS]
Options
- -s, --server <server>
Hostname to serve the site at.
- -p, --port <port>
Port to serve the site at.
Configuration
Exhibition draws configuration options from three places:
site.yaml
, which is the root configuration filemeta.yaml
, which there can be one or none in any given folder“Frontmatter”, which is a YAML header that can be put in any text file. It must be the first thing in the file and it must start and end with
---
- both on their own lines.
The difference between these different places to put configuration is explain in detail in the Getting started page.
Inheritance
One important aspect of Exhibition’s configuration system is that for a given node (a file or a folder), a key is search for in the following way:
The current node is checked for the specified key. If it’s found, it is returned. Otherwise carry on to 2.
The parent of the current node is checked, and if the specified key is not found here then its parent is checked the same way (and so on), until the root node is found.
If the root node does not have the specified key, then
site.yaml
is searched.Only once
site.yaml
has been searched is aKeyError
raised if the key cannot be found.
Mandatory
The following options must be present in site.yaml
:
content_path
This is the path to where Exhibition will load data from. It should have the same directory structure and files as you want to appear in the rendered site.
deploy_path
Once rendered, pages will be placed here.
Warning
content_path
and deploy_path
should only appear in site.yaml
.
General
ignore
Matching files are not processed by Exhibition at all. Can be a file name or a glob pattern:
ignore: "*.py"
As glob patterns are fairly simple, ignore
can also be a list of patterns:
ignore:
- "*.py"
- example.xcf
base_url
If your site isn’t deployed to the root of a domain, use this setting to tell Exhibition about the prefix so it can be added to all URLs
strip_exts
Specifies if certain files should have their extensions removed when being
referenced via Node.full_url
. When deploying, this does not change the
filename, so you will need to configure your web server to serve those files
correctly.
By default, this will be applied to files ending in .html
, to disable this
feature use the following:
strip_exts:
You can also specify multiple file extensions:
strip_exts:
- .html
- .htm
index_file
Specify the name file name of the “index” file for a directory. By default this
is index.html
, as it is on most web servers. If you change this settings,
be sure to update your web server’s configuration to reflect this change.
dir_mode
Specify the permissions of the directories created when generating the site. Default value is 0o755
.
file_mode
Specify the permissions of the files created when generating the site. Default value is 0o644
.
Filters
filter
The dotted path notation that Exhibition can import to process content on a node.
Exhibition comes with a number of filters, such as exhibition.filters.jinja2
.
filter: exhibition.filters.jinja2
You can also specify multiple filters, like this:
filter:
- exhibition.filters.jinja2
- - exhibition.filters.external
- "*.jpeg"
external_cmd: "cat {INPUT} | exiftool - > {OUTPUT}"
Here you can see that the jinja2 filter will be applied using its default glob
(*.html
) and the external command filter will be applied to JPEG images.
filter_glob
Matching files are processed by filter
if specified, otherwise this option
does nothing.
filter_glob: "*.html"
As glob patterns are fairly simple, filter_glob
can also be a list of
patterns:
filter_glob:
- "*.html"
- "*.htm"
- "robot.txt"
Filters specify their own default glob, refer to the documentation of that filter to find out what that is.
filter_glob
is ignored if filter
is a list of filters.
Jinja2
templates
The path where Jinja2 templates will be found. Can be single string or a list.
extends
If specified, this will insert a {% extends %}
statement at the beginning of
the file content before it is passed to Jinja2.
default_block
If specified, this will wrap the file content in {% block %}
.
markdown_config
Markdown options as specified in the Markdown documentation.
pandoc_config
Pandoc options as specified in the PyPandoc documentation for the convert_text
function.
External Command
external_cmd
The command to run. This should use the placeholders {INPUT}
and
{OUTPUT}
for the input and output files respectively. For example:
external_cmd: "cat {INPUT} | sort > {OUTPUT}"
Markdown
markdown_config
Markdown options as specified in the Markdown documentation.
Pandoc
pandoc_config
Pandoc options as specified in the PyPandoc documentation for the convert_text
function.
Cache busting
Cache busting is an important tool that allows static assets (such as CSS files) to bypass the browser cache when the content of such files is updated, while still allowing high value expiry times.
cache_bust_glob
Matching files have their deployed path and URL changed to include a hash of
their contents. E.g. media/site.css
might become
media/site.894a4cd1.css
. You can specify globs in the usual manner:
cache_bust_glob: "*.css"
As glob patterns are fairly simple, cache_bust_glob
can also be a list of
patterns:
cache_bust_glob:
- "*.css"
- "*.jpg"
- "*.jpeg"
To refer to cache busted nodes in your Jinja2 templates, do the following:
<link rel="stylesheet" href="{{ node.get_from_path("/media/css/site.css").full_url }}" type="text/css">
Filters
Exhibition comes with a number of filters. You can also write your own!
Filters are set by adding the dotted path to their module to the filter
key
in your configuration. See Configuration for more inforatmion.
Jinja2
exhibition.filters.jinja2
will process the contents of a node via the
Jinja2 templating engine. Check the Jinja2
documentation for syntax and a basic understanding of how Jinja2 works.
Unless it is set, filter_glob
will default to *.html
Context variables
- node
The current node
- time_now
A datetime object that contains the current time in UTC.
Meta
There are some meta options that are used exclusively by Jinja2:
- templates
Where to search for templates.
- extends
Automatically add an
{% extends %}
tag to the start of the content of every affected node.- default_block
Wrap the content of affected nodes with the specificity
{% block %}
tag.
Markdown
markdown
is provided as a Jinja2 filter and it can be configured via the
markdown_config
meta variable, which is passed to the markdown function as
keyword arguments.
Please view the Markdown documentation for details.
Pandoc
pandoc
is provided as a Jinja2 filter and can be configured by the
pandoc_config
meta variable, which is passed to the convert_text function
as keyword arguments.
Please refer pypandoc project for details.
Note, pypandoc requires pandoc to be installed. It will error without it.
Typogrify
All Typogrify filters are available. See the Typogrify webste for more details.
Exhibition specific filters
metasort
Given a list of nodes, metasort
will sort the list like this:
{{ node.children.values()|metasort("somekey") }}
Where somekey
is a key found in each node’s meta.
You can also reverse the order like so:
{{ node.children.values()|metasort("somekey", True) }}
metaselect
and metareject
Given a list of nodes, metaselect
will filter out nodes that either do not
have that key in their meta or do but the value resolves to something falsey.
For example, the following will filter out any nodes that have listable
set
to False
:
{{ node.children.values()|metaselect("listable") }}
metareject
works the same way, except it filters out nodes that don’t
have falsey values for the given key.
Marked sections
Marked sections are a great way to allow parts of your content to be referenced elsewhere, for example the preamble to a blog post:
---
title: My Post
---
{% mark intro %}
Blah blah blah…
{% endmark %}
Some more text
In another node you might want to list all the blog posts with their intros:
{% for child in node.children.values() %}
<h3>{{ node.meta.title }}</h3>
<p>{{ node.marks.intro }}</p>
{% endfor %}
You can have as many marks as you like in a node and they can be nested.
Raising Errors
Sometimes it can be useful to raise an error, especially if the logic in your template is quite complex!
{% if 2 == 3 %}
{% raise "This shouldn't be true! The Universe is broken!" %}
{% endif %}
Add Your Own Template Filters
An example:
from exhibition.filters.jinja2 import JinjaFilter
def emoji(input_string):
return input_string + "🖼️"
content_filter = JinjaFilter({"emoji": emoji})
This file should be a module that Exhibition can import and must be set in the configuration for any pages you want to use it.
Extending Jinja2 Filter Further
Extending the Jinja2 filter is much the same as adding your own template
filters. Simply subclass exhibition.filters.jinja2.JinjaFilter
,
override whatever methods you want and instanisate the class to
content_filter on your module. Then add your filter to your configuration in
place of the default Jinja2 filter.
External Command
The external command filter only has one option: external_cmd
, which is the
shell command to be run. The specified command should use {INPUT}
as the
input file and {OUTPUT}
as the output file, for example:
external_cmd: "cat {INPUT} | base64 > {OUTPUT}"
Unless it is set, filter_glob
will default to *.*
for this filter.
Markdown
The Markdown filter is a simple filter for those who don’t want to use Jinja2.
This filter can be configured via the markdown_config
meta key, which is
passed to the markdown function as keyword arguments.
Please view the Markdown documentation for details.
Pandoc
The Pandoc filter is a simple filter that can a file from one format to
another, e.g. rendering a LaTeX document to a PDF. It can be configured by the
pandoc_config
meta variable, which is passed to the convert_text function
as keyword arguments.
Please refer pypandoc project for details.
Note, pypandoc requires pandoc to be installed. It will error without it.
Make Your Own
To create your own filter for Exhibition, your module must implement a function with the following signature:
def content_filter(node, content):
return ""
- node
is the current node being processed.
- content
is the content of that node, with any frontmatter removed.
content_filter
should return a string, which will then become the rendered
form of this node.
As we saw in Extending Jinja2 Filter Further, a filter can also be written
as a class. You can write a filter in any way you like as long as you end up
with a module that has a callable named content_filter
. You can take a look
at exhibition.filters.base.BaseFilter
for an example of a class based
filter.
exhibition
exhibition package
Subpackages
exhibition.filters package
External command filter
Use an external command to process a file, like so:
filter: exhibition.filters.external
external_cmd: sed 's/this/that/g' {INPUT} > {OUTPUT}
Jinja2 template filter
To use, add the following to your configuration file:
filter: exhibition.filters.jinja2
- class exhibition.filters.jinja2.JinjaFilter(extra_filters=None)[source]
Bases:
BaseFilter
This is the actual content filter called by
exhibition.main.Node
on appropriate nodes.- Parameters
node – The node being rendered
content – The content of the node, stripped of any YAML frontmatter
extra_filters
should be a dict. The keys are the name of the filter and the values are the template filters- extensions = (<class 'exhibition.filters.jinja2.RaiseError'>, <class 'exhibition.filters.jinja2.Mark'>)
- prepare_content()[source]
Prepares content by adding
{% extends %}
and a default block, if either are specified
- template_loader_class
alias of
FileSystemLoader
- class exhibition.filters.jinja2.Mark(environment: Environment)[source]
Bases:
Extension
Marks a section for use later:
{% mark intro %} <p>My Intro</p> {% endmark %} <p>Some more text</p>
This can then be referenced via
Node.marks
.- identifier: ClassVar[str] = 'exhibition.filters.jinja2.Mark'
- parse(parser)[source]
If any of the
tags
matched this method is called with the parser as first argument. The token the parser stream is pointing at is the name token that matched. This method has to return one or a list of multiple nodes.
- tags: Set[str] = {'mark'}
if this extension parses this is the list of tags it’s listening to.
- class exhibition.filters.jinja2.RaiseError(environment: Environment)[source]
Bases:
Extension
Raise an exception during template rendering:
{% raise "This is an error" %}
- identifier: ClassVar[str] = 'exhibition.filters.jinja2.RaiseError'
- parse(parser)[source]
If any of the
tags
matched this method is called with the parser as first argument. The token the parser stream is pointing at is the name token that matched. This method has to return one or a list of multiple nodes.
- tags: Set[str] = {'raise'}
if this extension parses this is the list of tags it’s listening to.
- exhibition.filters.jinja2.metasort(nodes, key=None, reverse=False)[source]
Sorts a list of nodes based on keys found in their meta objects
- exhibition.filters.jinja2.pandoc(ctx, text, fmt=None)[source]
Use Pandoc to convert from one format to another. Takes source format as an optional argument.
Uses the same
pandoc_config
meta key asexhibition.filters.pandoc
Markdown filter
To use, add the following to your configuration file:
filter: exhibition.filters.markdown
Pandoc filter
To use, add the following to your configuration file:
filter: exhibition.filters.pandoc
pandoc_config:
format: org
format
can be any format that Pandoc supports
Submodules
exhibition.command module
Documentation for this module can be found in exhibit commandline script
exhibition.config module
- class exhibition.config.Config(data=None, parent=None, node=None)[source]
Bases:
object
Configuration object that implements a dict-like interface
If a key cannot be found in this instance, the parent
Config
will be searched (and its parent, etc.)- Parameters
data – Can be one of a string, a file-like object, a dict-like object, or
None
. The first two will be assumed as YAMLparent – Parent
Config
orNone
if this is the root configuration objectnode – The node that this object to bound to, or
None
if it is the root configuration object
exhibition.node module
- class exhibition.node.Node(path, parent, meta=None)[source]
Bases:
object
A node represents a file or directory
- Parameters
path – A
pathlib.Path
that is either thecontent_path
or a child of it.parent – Either another
Node
orNone
meta – A dict-like object that will be passed to a
Config
instance
- add_child(child)[source]
Add a child to the current Node
If the child doesn’t already have its
parent
set to this Node, then anAssertionError
is raised.
- property cache_bust
- property content
Get the actual content of the Node
If
filter
has been specified inmeta
, that filter will be used to further process the content.
- property data
Extracts data from contents of file
For example, a YAML file
- classmethod from_path(path, parent=None, meta=None)[source]
Given a
pathlib.Path
, create a Node from that path as well as any children. Children are loaded in Unicode codepoint order - this order is preserved inNode.children
if you’re unsure what that means.If the path is not a file or a dir, an
AssertionError
is raised- Parameters
path – A
pathlib.Path
that is either thecontent_path
or a child of it.parent – Either another
Node
orNone
meta – A dict-like object that will be passed to a
Config
instance
- property full_path
Full path of node when deployed
- property full_url
Get full URL for node, including trailing slash
- get_from_path(path)[source]
Given a relative or absolute path, return the
Node
that represents that path.- Parameters
path – A
str
orpathlib.Path
- property index_file
- property marks
Marked sections from content
- property meta
Configuration object
Finds and processes the YAML front matter at the top of a file
If the file does not start with
---\n
, then it’s assumed the file does not contain any meta YAML for us to process
- render()[source]
Process node and either create the directory or write contents of file to
deploy_path
- property siblings
Returns all children of the parent Node, except for itself
- property strip_exts
exhibition.utils module
Module contents
Security
If you find a security issue with Exhibition, email security@moggers87.co.uk. If you want to send an encrypted report, then please use key id 0x878B5A2A1D47C084.
Exhibition follows the same security reporting model that has worked for other open source projects: If you report a security vulnerability, it will be acted on immediately and a fix with complete full disclosure will go out to everyone at the same time.
Changelog
0.2.3
Fixed
Add missing data files to package manifest
0.2.2
Added
Add pandoc and markdown content filters
Add file and directory permission configuration
Add support for specifying multiple filters at a time
Add Python 3.10 support
Add support for Jinja2 version 3.0.x
Changed
Change Jinja2 content filter from a function to a class
Custom filters can now inherit from this class if they wish
Fixed
Fix site template not being part of Python package
Removed
Removed tests against Jinja2 version 2.9
It might still work, but we don’t consider any breakages to be bugs.
0.2.1
This release is just for a fix some bad syntax in our documentation. Nothing else has changed!
0.2.0
Added
Add support for a pandoc filter for Jinja2 templates
exhibit create
command to get users started
Removed
Remove support for Python 3.5, 3.6, and 3.7
Fixed
Add description to
exhibit
commandUse
cached_property
to make code cleaner
0.1.1
Added
Allow settings of local HTTP server address and port.
Added Python 3.8 support.
Removed
Nothing was removed in this release
Fixed
Set
Cache-Control
tono-store
on HTTP server responesHTTP server should ignore
GET
params and fragments
0.1.0
The “I’d almost recommand it to my friends” release.
Added
Added Python 3.7 support.
Add the external command filter.
Document Jinja2 filter.
Add
strip_exts
as an user configurable setting.Add
index_file
as an user configurable setting.
Removed
Removed Python 3.4 support.
Fixed
Reorganised package so that code is easier to manage.
Make node loading deterministic, meta files loaded first and then alphabetical order for the rest.
0.0.4
Added vesrioneer.
Fix bug where
exhibit serve
was not serving files with extension stripping enabled.A
KeyError
raised byConfig
now display the path of the node they are attached to, making debuging missing keys far easier.Improved test coverage and fixed numerous bugs.
Implemented cache busting for static assets (images, CSS, and such). Use the
cache_bust_glob
option to control which files are cache busted.Implemented
Node.get_from_path
which can fetch aexhibition.main.Node
specified by a path.Make all Exhibition defined meta keys use underscores not hyphens.
0.0.3
Fix bug where extension stripping was not being applied.
0.0.2
Fixed trove classifiers.
Add
__version__
toexhibition.__init__
.
0.0.1
Everything is new! Some choice features:
Configuration via YAML files and YAML front matter.
Jinja2 template engine is provided by default.
A local HTTP server for development work.
Less than 2000 lines of code, including tests.