Pelican blog: up and running

Pelican is a static site generator written in Python. I use it for my personal blog. Pelican is easy to install, configure and customize with themes and plugins. I was able to set up the blog with my own custom theme and an Open Graph plugin in a few hours.

Before Pelican I also used Jekyll and Octopress. But since my first language is Python and Ruby ecosystem seems to be in a decline, I deciced to stick to a framework where I can express myself as idiomatic as I can. Last but not least, having to write documentation for Python code with Sphinx I also got used to ReStructuredText format, which is supported by Pelican out of the box.

In this post I’m not going repeat the Pelican’s great documentation. Instead I will focus on the step that allows you to:

  • Set up your blog quickly
  • Add your own theme to it
  • Extend Pelican’s basic functionality with a plugin
  • Automate build and deploy process, and
  • Host your static site with GitHub Pages under your custom domain

I use Pelican verion 4.0.1, invoke 1.2.0 and ghp-import 0.5.5 in this post.

Install Pelican

Personally I use pyenv for Python version management. I also use pyenv-virtualenv plugin to incorporate virtualenv into it. So my workflow for the Python projects that do not require containerization is the following:

# Create directory for the project, cd to it
$ mkdir my-project && cd my-project

# Set up virtual env with Python version needed
$ pyenv virtualenv 3.7.2 venv

# Activate virtualenv
$ pyenv activate venv

# Initialize git reposotory, add README and basic .gitignore
$ git init
$ touch README.md
$ echo '*~' > .gitignore
$ git commit -m 'Initial commit'

Now that a repository for your project is ready, install pelican package, fix its version and initialize the app:

$ pip install pelican
$ pip freeze | grep pelican > requirements.txt
$ git commit -a -m 'Fix pelican version'
$ pelican-quickstart

All the questions are pretty straightforward. One thing I would advise though is to confirm that you do want to automate site generation and publication. This step will create a Makefile and invoke’s tasks.py

Custom Theme

You can find a ton of Pelican themes in the wild. It’s so overwhelmingly huge amount of custom themes that I had just given up on finding a theme that’s just right for me. Instead I’ve build my own custom theme. It’s responsive, lightweight and clean. For rapid development I used Skeleton, which is a modern CSS boilerplate.

Although it’s recommended to install your theme either by copying it to the Pelican’s theme path or by creating a symlink, I used git submodule add theme in my blog repo directory. Then I added a path to the theme under THEME variable in pelicanconf.py:

$ cd my-project
$ git submodule add https://github.com/pilosus/pilosus-pelican-theme.git themes/pilosus-pelican-theme
$ echo "THEME = 'themes/pilosus-pelican-theme'" >> pelicanconf.py

Now my custom theme is versioned and can be easily developed separately from the blog. The blog may be generated with whatever theme version I like. I will only need to checkout my submodule to the version I need.

Plugins

The first thing I was disappointed in Pelican’s default theme is a lack of Open Graph tags. You cannot just add custom metadata to your content files and use it in the theme. The easiest way to tackle this problem is plugins.

I derived my Open Graph plugin from this one by tweaking the things I didn’t like. Say, the original plugin gets og:image by parsing images from the rendered HTML content, which is strange, ineffective and will certainly produce low-quality results in many cases. That’s why I decided to develop my own plugin.

Again, I find installing plugin as a git submodule a great way to keep the code versioned, site deployments deterministic and maintanence predictable and easy.

I place plugins under plugins directory in my blog repo, then define PLUGIN_PATHS and PLUGINS variables in the settings:

$ cd my-project
$ git submodule add https://github.com/pilosus/pilosus_pelican_og plugins/pilosus_pelican_og
$ echo "PLUGIN_PATHS = ['plugins']" >> pelicanconf.py
$ echo "PLUGINS = ['pilosus_pelican_og',]" >> pelicanconf.py

Build and Deploy Automation

Our goal is to generate a static site, that can be pushed and served by the GitHub Pages with custom domain support. To make this process less tedious some automation is essential.

If you have followed an advice in the Install Pelican section to opt for automation, then you’ve got tasks.py and Makefile installed. Although I do use Makefile in some of my projects I decided to give invoke a try. So the following recipe is all about tasks.py that invoke uses.

We need to install invoke package in your virtualenv, as well as ghp-import for GitHub Page push. Don’t forget to fix all dependencies in the requirements.txt:

$ cd my-project
$ pyenv activate venv
$ pip install invoke
$ pip freeze | grep invoke >> requirements.txt
$ pip install ghp-import
$ pip freeze | grep invoke >> requirements.txt

Now that we have all dependencies installed let’s add a new task for GitHub Pages deployment:

@task
def github(c):
  """Publish to GitHub Pages"""
  preview(c)
  cname(c)
  c.run('ghp-import -b {github_pages_branch} '
        '-m {commit_message} '
        '{deploy_path} -p'.format(**CONFIG))
  c.run('git push --force {github_repo} '
        '{github_pages_branch}:{github_repo_target_branch}'.format(**CONFIG))

preview(c) is a predefined invoke task that generates static files for production environment (i.e. with publishconf.py settings file).

cname(c) is a task that generates a file called CNAME with a hostname of your custom domain:

@task
def cname(c):
  """Generate CNAME file with you custom domain name

  Its used in GitHub Pages. Otherwise custom domain name setting
  gets reset on each git push to GH Page repo.
  """
  c.run('echo {custom_domain_name} > {deploy_path}/CNAME'.format(**CONFIG))

cname docstring says it all. You really need this file!

ghp-import command checks out you output directory (i.e. the one used for generated static files) to the GitHub Pages branch of your project. Then git push --force pushes this branch to the repository for static GitHub Pages (it should be named as your-github-login.github.io).

Configuration you use throughout the invoke’s tasks.py may look like this:

CONFIG = {
    # Local path to content directory
    'content_path': 'content',
    # Local path configuration (can be absolute or relative to tasks.py)
    'deploy_path': 'output',
    # Github Pages configuration
    'github_pages_branch': 'gh-pages',
    'github_repo_target_branch': 'master',
    'github_repo': 'git@github.com:pilosus/pilosus.github.io.git',
    'custom_domain_name': 'blog.pilosus.org',
    'commit_message': "'Publish site on {}'".format(datetime.date.today().isoformat()),
    # Port for `serve`
    'port': 8080,
}

Keeping two separate repositories (one for the blog and another one for the static files it generates) allows you to make your blog repository private. You can keep some secrets or some code you don’t want to share. But still your generated files will be accessible to others in the second repository.

Pelican Workflow

Now that we have defined all the invoke tasks, we can discuss Pelican’s workflow. It’s very straight forward:

  1. Write your content
  2. Rebuild the site with development settings: invoke rebuild
  3. Serve website at localhost: invoke serve
  4. Go to http://localhost:8000 check if everything is okay
  5. Clean output directory: invoke clean
  6. Build website with production settings and upload to the GitHub Pages: invoke github
  7. Enjoy your blog!

Custom Domain

By now you already have your static site live on your-github-login.github.io. In order to serve it under custom domain you need to add a CNAME record in your DNS and set custom domain in the settings of your-github-login.github.io repository on GitHub. Enforce HTTPS is also a great option to turn on.

What’s next?

Althouh the set up and automation I described here are pretty convenient, one thing could still be improved. We could use Travis CI or Circle CI to generate static files and push to the proper repository. CI pipeline should be triggered on each push to the remote blog repository.