The previous article walked through how to build a semi-practical teaser (thumbnail) image plugin. With the basics down, this article will demonstrate how to improve that plugin, making it more customizable and generalized.

Working with Settings in Plugins

Allowing users to set options for a plugin within their Pelican settings file is a good way to vastly increase the usefulness of a plugin. Fortunately, setting and getting settings within plugins is simple.

The big caveat to remember with settings is that early on in the generation process they are copied to context. If you add or change a setting after this point, code or templates that rely on the version in context may not see it correctly or at all. In practice, this shouldn't matter much because a setting should never be changed (without a very good reason). But, keep in mind that it is best to add or alter any settings in a function connected to the initialized signal to avoid this issue.

Adding Settings to the Teaser Plugin

Going back to the teaser plugin, the two URL constants are ideal candidates to be turned into settings that a user may customize. This would allow the user to specify where their teaser images are.

"""
teaser.py
"""
import logging
from pelican import signals

logger = logging.getLogger(__name__)

def check_settings(pelican):
    """
    Log warnings as needed for missing/malformed settings.
    """
    teaser_url = pelican.settings.get('TEASER_URL', '')

    if teaser_url and teaser_url[-1] != '/':
        logger.warning(
          'TEASER_URL setting does not end in forward slash (/). ' +
          "Plugin 'teaser' may not behave as expected."
        )

def normalize_teaser(generator, content):
    """
    Ensure that `content` (an article) has a valid teaser_img attribute.

    Gets the teaser_img metadata from each article, assumed to be a filename 
    of an image residing in path defined by TEASER_URL setting.

    If no teaser_img metadata found, teaser_img is set to DEFAULT_TEASER 
    setting, or None if setting not provided.
    """
    teaser_filename = content.metadata.get('teaser_img', None)
    teaser_path = content.settings.get('TEASER_URL', '')

    if teaser_filename:
        # If teaser_path present, append teaser_filename to it. 
        # If teaser_path not set, url becomes teaser_filename alone
        url = teaser_path + teaser_filename
    else:
        url = content.settings.get('DEFAULT_TEASER', None)

    content.teaser_img = url

def register():
    """Register the plugin with Pelican"""
    signals.initialized.connect(check_settings)
    signals.article_generator_write_article.connect(normalize_teaser)

A few things changed here. First, a new function, check_settings, verifies that the value of the user-specified TEASER_URL setting is what the plugin expects — a URL with a trailing slash — and logs a warning message if it is not.

The normalize_teaser function works essentially the same as it did before, but now gets the values of the path and default image from Pelican's settings, which is available via content.settings. In this case, the settings are also available via generator.settings. Any argument that comes to a signal callback probably has a settings attribute.

Given the above plugin, a settings file might look like this:

"""
pelicanconf.py
"""
# [rest of file omitted]

# teaser plugin settings
TEASER_URL = '/static/images/'
DEFAULT_TEASER = '/static/images/default_teaser.png'

url_format

When making settings for your own site, you may have noticed that URL settings can be dynamic based on content metadata, as explained here in the docs.

Fortunately, it's easy to provide that functionality for the TEASER_URL setting as well. This small step greatly extends the capability of the plugin, as it would allow teaser images to lie in different directories, defined by each article's metadata, rather than only a single directory.

On this site, for example, posts use the URL schema /posts/[year]/[month]/[slug]/, and static images for the posts (including teaser images) reside at /static/images/blog/[slug]/. Assuming the teaser images would reside in this same directory, the setting would look like this:

TEASER_URL = '/static/images/blog/{slug}/'

Changing the plugin to fill in the metadata and provide the correct URL for each article only requires altering one line of normalize_teaser:

def normalize_teaser(generator, content):
    """
    Ensure that `content` (an article) has a valid teaser_img attribute.

    Gets the teaser_img metadata from each article, assumed to be a filename 
    of an image residing in path defined by TEASER_URL setting.

    If no teaser_img metadata found, teaser_img is set to DEFAULT_TEASER 
    setting, or None if setting not provided.
    """
    teaser_filename = content.metadata.get('teaser_img', None)
    teaser_path = content.settings.get('TEASER_URL', '')

    if teaser_filename:
        # If teaser_path present, append teaser_filename to it. 
        # If teaser_path not set, url becomes teaser_filename alone
        url = teaser_path.format(**content.url_format) + teaser_filename    # THIS LINE CHANGED
    else:
        url = content.settings.get('DEFAULT_TEASER', None)

    content.teaser_img = url

This change might look a bit like magic because it essentially is. Pelican provides the url_format property for each content object, which returns a dictionary of metadata which can then be passed to the format string.

Plugin Packages

Though plugins can be single modules, as shown so far with teaser.py, it's good practice to make them as packages — especially if you plan on ever distributing them.

To convert the teaser plugin to a package, set it up with the following directory structure:

[project root]/
  plugins/
    teaser/
      __init__.py 
      teaser.py   # Same file as before, simply moved

The file __init__.py should contain the line from .teaser import register. That is the bare minimum necessary to make a plugin package. No change has to be made in settings, assuming the package name (the folder it's contained in) does not differ from the module name.

Conclusion

With those few steps, the teaser plugin has been considerably impoved. On top of the initial specifications, the URL and default image settings are now user-customizable, the URL setting is dynamic, appropriate fallbacks are in place, and the plugin has been turned into a more best-practice-friendly package.

A future article in this series will detail how to build a more elaborate plugin with a custom generator for specialized input files.