Introduction
Pelican is a static site generator written in Python, which was used to create this site. Extending Pelican with plugins can be a powerful tool, but unfortunately when going to create my own first plugin, I found documentation to be somewhat lacking. So, I'd like to provide a few tutorials demonstrating how I built plugins for this site, starting with a very simple one.
This article assumes basic familiarity with Python, and working with Pelican to create a basic blog site, but does not assume knowledge of Pelican internals.
Key Concepts
Before delving into plugins, there are a few Pelican concepts that developers should be familiar with.
First, it's a good idea to have a look through the Pelican internals page to get a sense of overall structure.
The following are some further concepts that will hopefully expand upon and clarify what's in the documentation.
Template Data Sources
Often, a plugin will want to make available some variable to templates, or manipulate an existing one (e.g. article metadata). It's important to understand the two major ways to make data visible to templates.
Context
Pelican uses a global context
object — a dictionary — which holds relevant data (such as all the Article
objects in context['articles']
) necessary to build the site. This context object is passed between all generators, which typically add to or update it to define what articles/pages to write.
The context
is sent to writers, which use its data to populate templates. When one writes something like {% for a in articles %}
in a template, context['articles']
is what is being accessed behind-the-scenes.
The context
contains a copy of the Pelican settings dictionary, so any setting can be accessed by name in a template. If you want to change settings in a plugin, it should be done when the initialized
signal is sent (more on signals later), since that is before the settings are copied to context
. User-defined settings for a plugin related to presentation can thus be placed in the regular Pelican settings file.
In short, if a plugin needs to make a variable available globally or across many different pages, context
is probably the place to put it.
Content Attributes
The other way to send data to a template is to make it an attribute of an object visible to the template. This is most often seen in the case of articles or pages (which are both "content" types in Pelican).
You've probably seen that in the article.html
template, an {{ article }}
variable is available. This is the Article
object itself, which is sent automatically as a variable by Pelican. The full list of variables available to various templates by default is here. If you set a custom attribute on that object in a plugin (e.g. article.foo = 'bar'
), then {{ article.foo }}
is available in any template that gets article
.
All metadata for pages or articles become attributes of the respective content object. This is what makes e.g. {{ article.title }}
available in a template.
When a plugin needs to set unique variables for individual articles or pages, it's best to do so on the content object, rather than in context
.
Generators
Note: Any reference to generators herein is referring to Generator
objects defined by Pelican, not the more general concept of generators in Python
Generators are essentially the middleman between Reader
objects, which convert the content and metadata of input files (e.g. article or page markdown files) to something Python-friendly, and Writer
objects which actually render and write output files from templates.
Generators ultimately control the flow of the bulk of what Pelican does. They are what actually iterate through each article/page, invoking readers and writers appropriately. In fact, examining a portion of the main run()
function of Pelican (full source here), one can see that it mostly involves invoking operations on generators:
# Note global context includes all settings options context = self.settings.copy() # [...] # Initialize all generators generators = [ cls( context=context, settings=self.settings, path=self.path, theme=self.theme, output_path=self.output_path, ) for cls in self.get_generator_classes() ] # Generate context for each generator # (read in input files; update global context) for p in generators: if hasattr(p, 'generate_context'): p.generate_context() # [...] signals.all_generators_finalized.send(generators) writer = self.get_writer() # Write output of each generator for p in generators: if hasattr(p, 'generate_output'): p.generate_output(writer) signals.finalized.send(self)
By default, Pelican includes three major generators: ArticlesGenerator
, PagesGenerator
, and StaticGenerator
. Each is responsible for handling their respective input types. However, any number of custom generators can be added. As one might guess from the snippet above, custom generators should implement one or both of the generate_context
and generate_output
methods. An example of building a custom generator will be in a future article in this series.
Signals
Key to developing Pelican plugins is understanding signals. As the site is being generated, signals are dispatched by Pelican which notify any listeners that some key part of the generation process has occurred (or is about to occur).
For example, in the code snippet in the previous section, the signals.all_generators_finalized.send(generators)
line dispatches the all_generators_finalized
signal after generate_context
has been called on each generator.
When writing plugins, you won't be sending signals, you will be listening for them. More on how to do this later.
A complete list of signals can be found here in the official docs. As you can see, the names of the signals often refer to the processes of generators, readers, and writers that were discussed previously.
A key component of writing a new plugin is determining which signal to connect to, based on when in the process you need to do work, and what data you need to work with. This is something I found particularly confusing when starting out with Pelican, and was partially the inspiration for these tutorials.
Plugin Basics and Setup
If you haven't already, I recommend reading through the official documentation page on plugins.
Here is a general process for creating a Pelican plugin, assuming you created your site with the pelican-quickstart
command.
-
Create a
plugins
folder in your project's root directory (the one withpelicanconf.py
). -
In
pelicanconf.py
, add the linePLUGIN_PATHS = ['plugins']
-
Create a package or module in the
plugins
folder that contains aregister()
function. -
Add your plugin to the list of installed plugins. In
pelicanconf.py
, add the linePLUGINS = ['your_plugin_name_here']
. This name is whatever you named your plugin's module/package.
Minimal Example
Here is a barebones example of a plugin:
""" exampleplugin.py """ import logging from pelican import signals logger = logging.getLogger(__name__) # This function called when all_generators_finalized signal sent def fn(generators): logger.info('\n\nHello World! All generators finalized\n\n') # A register() function must be present in all plugins, and will be called # automatically when Pelican initializes plugins def register(): # Connect to all_generators_finalized signal # Argument to connect() is the callback function to be called # when signal is sent signals.all_generators_finalized.connect(fn)
This plugin does, well, nothing. It simply logs a message when the all_generators_finalized
signal is sent. But it serves as the smallest reasonable plugin that one could make.
Something important to note in this example is that when the callback function connected to the signal is called, an argument (or multiple arguments) will be passed to it. What exactly is passed depends on the signal. In the case of all_generators_finalized
, a list of all generator objects is passed. The aforementioned list of signals specifies what data comes with each signal.
Try installing the plugin above by following the steps in the previous section. If the code above was in plugins/exampleplugin.py
, it could be installed by adding the following two lines to the settings file:
PLUGIN_PATHS = ['plugins'] PLUGINS = ['exampleplugin']
Once the plugin is installed, if you run pelican --debug content
to re-generate your site, you should see the 'Hello World...'
message somewhere in the output.
Conclusion
The next article in this series will demonstrate building a more practical, yet still simple, plugin.