Writing a Pelican Plugin to output a carousel

Pelican is deliberately lightweight and therefore doesn’t necessarily have everything you might want from it out of the box. I use it to generate my portfolio and projects pages and now, thanks to my increasing frustration with WordPress my blog, it’s quick and easy to use particularly for things like this which don’t need a full CMS backing them. However, for my portfolio page I wanted each project to have a carousel of images associated with it, something which Pelican can’t do natively.

So I wrote a very simple plugin which literally just takes a comma separated list of image files and allows you to output them in the format you prefer.

Input

Carousel: image1.jpg, image2.png, image3.svg, does_not_exist.png, no_file_extension, wrong_file_type.pdf

The input should be a list of image files which you place into content > images however human error being what it is, the plugin should first check to make sure:

  • that the file has an extension
  • that the extension is in the list of acceptable image files
  • that the image does in fact exist

only if those conditions are met should it output the image url.

Output

for image in article.carousel should iterate over the valid image files in the list.

Jinja2 / Bootstrap 4 carousel

{% if article.carousel %}
  <div class="carousel slide rounded" id="{{ article.slug }}Carousel" data-interval="false">
    <div class="carousel-inner" role="listbox">

      {% for image in article.carousel %}
        <div class="carousel-item{{ ' active' if loop.index == 1 }}">
          <img class="d-block img-fluid rounded" src="{{ image }}">
        </div>
      {% endfor %}

      <a class="carousel-control-prev" href="#{{ article.slug }}Carousel" role="button" data-slide="prev">
        <i class="fa fa-chevron-left" aria-hidden="true"></i>
        <span class="sr-only">Previous</span>
      </a>
      <a class="carousel-control-next" href="#{{ article.slug }}Carousel" role="button" data-slide="next">
        <i class="fa fa-chevron-right" aria-hidden="true"></i>
        <span class="sr-only">Next</span>
      </a>
    </div>
  </div>
{% endif %}

The back end

In order to make a plugin for Pelican the only thing that is absolutely required is to define a register callable that to map a signal to your plugin logic. There’s a fair number of signals and which one you need depends on what you’re trying to do. In this instance I want to jump in on article generation so the signal I need is article_generator_write_article.

Technically, once registered, via adding the following to pelicanconf.py:

PLUGIN_PATHS = ["plugins/carousel"]
PLUGINS = ["carousel"]

this plugin would work with just the following:

from pelican import signals

def generate_carousel():
  pass

def register():
    signals.article_generator_write_article.connect(generate_carousel)

But that’s neither terribly interesting nor useful. So lets make it do some stuff. If we want to be able to change defaults and check if images exist we need to do some additional setup:

EXTENSIONS = ['jpg', 'png', 'svg']

def set_defaults(settings):
    settings.setdefault('CAROUSEL_IMAGES_FOLDER', 'images')

def init_default_config(pelican):
    from pelican.settings import DEFAULT_CONFIG
    set_defaults(DEFAULT_CONFIG)
    if pelican:
        set_defaults(pelican.settings)

In order to use the data in the article you need to pass it to your function:

def generate_carousel(generator, content):
if 'carousel' in content.metadata.keys():
      if len(content.metadata['carousel']) > 0:
          carousel = content.metadata['carousel'].split(',')
          del_these = []
          folder = generator.settings.get('CAROUSEL_IMAGES_FOLDER')

          # remove all whitespace and check that the filename is valid
          for idx, item in enumerate(carousel):
              if item[-3::] in EXTENSIONS and exists('{}/{}/{}'.format(generator.settings['PATH'], folder, item.lstrip())):
                  carousel[idx] = '{}/'.format(folder) + item.lstrip()
              else:
                  del_these.append(item)

          # remove any filenames with extensions that aren't in the list
          for item in del_these:
              carousel.remove(item)

          # make it accessible from the app context
          content.carousel = carousel

Make sure the defaults are registered.

def register():
    signals.initialized.connect(init_default_config)
    signals.article_generator_write_article.connect(generate_carousel)

Add a __init__.py to the folder with the following content:

from .carousel import *

Congratulations, you have a pelican plugin.

Disclosure

As an Amazon Associate I earn from qualifying purchases.