Context

When porting my blog from Drupal to Pelican, I wanted to preserve my clean URLs as much as possible for old blog entries. In Pelican you typically achieve this by making sure each article has a slug metadata entry with the desired clean URL and setting up _SAVE_AS and _URL configs in pelicanconf.py for example as follows:

ARTICLE_SAVE_AS = '{slug}/index.html'
ARTICLE_URL = '{slug}/'

In addition (or instead) you can also play with rewrite rules, if you have control over this on your setup. Because I'm planning to host my blog on GitHub Pages, I don't.

However, the above ARTICLE_URL setting means that each article will produce a directory with a single index.html file, which felt a bit too much as overkill to avoid where not necessary.

So config-wise, for new articles I wanted something more like this (the default):

ARTICLE_SAVE_AS = '{slug}.html'
ARTICLE_URL = '{slug}.html'

How to combine these?

Dynamic Pelican settings

Turns out it is possible to combine both with "dynamic" settings that allow you to add a touch of logic to the configuration settings. The trick below is based on the fact that Pelican interpolates config variables like ARTICLE_SAVE_AS and ARTICLE_URL to actual values for each article with something like

setting.format(**metadata)

So we just have to assign objects with a custom .format(**kwargs) method to the config variables.

First, let's define this helper class:

class DynamicSetting(object):
    def __init__(self, f):
        self.f = f
    def format(self, **metadata):
        return self.f(metadata).format(**metadata)

On construction of a DynamicSetting object, we pass it a function f to generates a variation of the setting (e.g. {slug} or {slug}.html) based on metadata.

For example, say that for articles where we want to enforce a certain clean URL, we use a custom metadata entry CleanUrl, like:

Title: hello world
CleanUrl: hello

Hello world

and for articles where we don't need a particular clean URL we don't.

With the helper class of above, we can now define ARTICLE_SAVE_AS and ARTICLE_URL dynamically as follows:

ARTICLE_SAVE_AS = DynamicSetting(lambda metadata: 
    '{cleanurl}/index.html' if 'cleanurl' in metadata else '{slug}.html'
)
ARTICLE_URL = DynamicSetting(lambda metadata: 
    '{cleanurl}/' if 'cleanurl' in metadata else '{slug}.html'
)

So for articles where there is CleanUrl metadata we'll use that for output directory path and URL. For other articles we fall back on the normal {slug}.html way.

Note that it is also possible to use the helper as a decorator

@DynamicSetting
def ARTICLE_URL(metadata):
    if 'cleanurl' in metadata:
        return '{cleanurl}'
    else:
        return '{slug}.html'

which gives bit more breathing room for the custom logic.