Building a Static Site with Pelican

Posted on July 14, 2018

In this post, my plan is to provide a comprehensive walkthrough of how I generated this site on my Windows machine. As I was going through tutorials, I found a lot of them were not very clear, or I had to really dig to find the information I was looking for. My hope is that by putting all of the information I managed to scrap together in one place, I can spare some other poor OCD soul the heartache I endured. The source code is available for reference at GitLab, which might be enough of a tutorial. If you want a little bit (or a lot) more, though, continue reading!

The first thing to be done is to set up your environment. Personally, I like having separate environments for separate things. I'm also familiar with Anaconda so I decided to go that route. Although this website is built with Pelican for Python 2.7, I downloaded Anaconda for Python 3.6 (latest at time of writing) because it is backward-compatible. Choose your install directory, and pick the other install options (I went with defaults).

Once Anaconda is installed, go ahead and open "Anaconda Prompt". Now, create an environment using conda create -n pelican python=2.7. Here, pelican is the environment name, and python=2.7 tells conda to use python 2.7. Now that your environment is ready you need to activate it using activate pelican. Now we can start getting the libraries we need using pip. Installing libraries is simple! Firstly, you need to add Pelican: pip install pelican. I also installed Fabric. This lets you use a fabfile instead of a Makefile for building site content. I switched to Fabric because I wasn't having much luck with getting the Makefile to work properly and I didn't want to invest a lot of time researching it. I'm reasonably familiar with Python so I installed Fabric and switched to a fabfile.

Now we get to the process of building out the site. I like having a lot of control, so I did away with a lot of the default behaviors. Let's go through my pelicanconf.py.

The Config File

The first thing I added in was TIMEZONE1.

TIMEZONE = 'US/Central'

The first modification I made was to use a dictionary instead of a collection of tuples for my links:

LINKS = {'LinkedIn' : 'https://www.linkedin.com/in/andrew-interdisciplinarian'}

I had a hard time getting Jinja to work with the tuples, it was more intuitive to iterate over a dictionary like so (code pulled from base.html in source):

{% for site, url in LINKS.items() %}
<span class="separator"></span>
<a href="{{ url }}">{{ site }}</a>
{% endfor %}

.items() gets an enumeration of the dictionary entries, and the keys and values are referenced as site and url respectively. Then you just call them as you typically would with Jinja. Easy. The next setting orders articles by date created descending, pretty straightforward. The next modified line is the PAGE_ORDER_BY = 'navorder'.

navorder is a bit of metadata I added to all of my pages to control how they display from left to right in the navbar. If you look at my src/content/pages folder, every file has a :navorder: attribute. The next thing I modified is how dates are formatted, which I used Wikipedia to figure out2.

I added in the LOAD_CONTENT_CACHE = False line so I didn't have to do a hard refresh on the browser to make it reload javascript files. I'm going to be honest, I'm not 100% sure it works, but I think it does.

I wanted to keep my posts and projects in separate folders, but I wanted both to get picked up by the routine that parses articles, so that's why you see posts and projects in the ARTICLE_PATHS array.

Next was specifying the theme of the site: I just threw in an absolute path to the theme directory. I mentioned that I did away with a lot of the default behaviors, and we're at the part of the config file where I did this:

ARCHIVES_SAVE_AS = ''
ARTICLE_SAVE_AS = '{category}/{slug}.html'
AUTHORS_SAVE_AS = ''
AUTHOR_SAVE_AS = ''
CATEGORY_SAVE_AS = ''
CATEGORIES_SAVE_AS = ''
DRAFTS_SAVE_AS = ''
INDEX_SAVE_AS = ''
PAGE_SAVE_AS = '{slug}.html'
TAG_SAVE_AS = ''
TAGS_SAVE_AS = ''

When you generate your site, Pelican creates a lot of default HTML files. Most of them I did not want. I got rid of everything except articles and pages for my site.

The last lines in my config file deal with static resources:

STATIC_PATHS = ['favicon.ico', 'assets']
EXTRA_PATH_METADATA = {
    'assets/favicon.ico' : {'path': 'favicon.ico'}
}

By setting STATIC_PATHS, you're telling Pelican which files and/or folders need to be copied directly from your source code to your output directory. By default, when Pelican copies files from your source code to your output, it will mirror the structure. Setting EXTRA_PATH_METADATA overrides this behavior.

A favicon needs to be at the root of a website, so if it's in the assets folder, it won't get picked up by the browser. Pelican uses a dictionary of dictionaries for these overrides. The base key is the path to the file whose tree structure you want to override, its value is another dictionary. The key of this sub-dictionary is the attribute we're modifying (path) and its value is the file tree structure we want relative to the root of the output folder.

That's everything for the pelicanconf.py file. In this folder there is also a publishconf.py file. literally copied my pelicanconf.py and added one line:

OUTPUT_PATH = '../site'

My motivation for this was I wanted two repositories, one for source code and one for the site. There are other (arguably better) ways to keep things separate, but this is how I chose to go about it.

Alright, so we're finally done with configuring how we want everything to get processed, now let's look at building our own theme!

Theme

If you're looking at the source code, mosy on over to src/theme. In this directory are two folders: static and templates. Static (as you probably have guessed) contains all the static resources. In my case, I'm rolling my own (albeit Bootstrap-esque) css and some JavaScript for my navbar. I decided to forego using Bootstrap to keep my css as transparent as possible to make it easier to debug. I have a lot of feelings about this but that's a topic for another blog post. On with the tutorial. Typically you would find an index template page, along with a couple of others. I got rid of all of those as part of my effort to do away with Pelican's default behavior and give myself more control. For clarity I named my index page "home". When you operate this way, though, you have to ensure that Jinja knows which template to use to build your html files. If you look in my src/content/pages directory, you'll notice that every page has a :template: attribute which tells Jinja which template to use to build the html.

When you're building a theme, the first thing you want to start with is a base.html file. You'll put all of your head matter in here, as well as references to your JavaScript files (arguably head matter). If you're not familiar with templating, every page you build will nestle its content within this base file, eliminating the need for repetitive code that's easy to forget. Since all of my pages will have a navbar and footer, I included these things in my base.html file. With this file, I just wanted to point out something about Jinja templating: it doesn't care about quotation marks. Typically I would worry about whether or not curly brackets within quotes would be interpreted differently than curly brackets without. It doesn't matter, though, just put those double angle brackets wherever you want to pull templating data and Jinja will figure it out. I recognize that my theme might be confusing, so I'll try to lay everything out. I mentioned that I have articles and projects, which are both picked up by the article processing routine, but I still want them on separate pages. Looking ahead, I thought I might want another page for recipes, and maybe a couple of other pages for various things, but my point is that the majority of my "pages" will essentially be "blogs", so as you go through my "page.html" file, you may notice it renders more like a blog might. I think this is probably the most obscure template in my theme, so let's dig into it a bit.

The first line, {% import 'sections/macros.html' as sections with context %}, tells Jinja that we want to use "macros.html" in this template, so we import it to make its contents accessible. It also says we want to access this file's contents using the alias "sections".

The second line tells Jinja that this page should be nestled into our "base.html" template (mentioned earlier).

Line 3 is a Jinja block. These blocks correspond to locations in the template file. In this "page.html" file, {% block title %} lets Jinja know that we want everything between the declaration tag and the {% endblock %} tag in the "base.html" file on line 7, where it has its own {% block title %} Jinja block. If a page inherits from a base page, every jinja block should also be present in the base page.

Moving down to line 5, we have {% if page.blogroll|string() == "Projects" %}. The part of this line I had a hard time with was the filter: |string(). This filter tells Jinja to take the metadata blogroll value and interpret it as a string. You need this because metadata are interpreted differently than strings. It took me a long time to figure that out.

The next line demonstrates how we use code from the "macros.html" file. We call the alias "sections", followed by whatever method we want to call that we defined in our file referenced by our alias. So here I'm calling the project_section() method. If we go into "sections/macros.html", we see a new type of Jinja block, {% macro project_section() %}, but it's not too hard to figure out. macro just tells Jinja that what's in this block can be pulled and used in different pages, and project_section() is the method call that actually tells Jinja to pull the content in.

The next macro in this file takes an argument: {% macro post_section(blogroll) %} You can then reference this argument within your macro with double curly brackets like any other Jinja variable.

Fabfile

The last thing to cover is the fabfile. The main thing I wanted to do was create a template for post/project generation. If you scroll to the bottom, you'll see the two methods that do just that. These methods essentially just generate metadata so I don't have to. You would call this like any other method in your fabfile from AnacondaPrompt: fab post:"A Whole New Post". It's important to enclose the title in double quotes or else only the first word will be parsed as an argument. I've written the title as I want it to appear to a reader on my site. Here's how the method works:

The project method is essentially the same except the template is a little different.

The only methods from this file I use are Build (to build the output) Rebuild (to delete things and then build the output), Serve (to view the site on my localhost) and Publish(to build Github Pages HTML). That's it, we're done!

Hopefully this has been helpful and worthwhile. I apologize if the last section was more terse than the others, there has been a long break between them and I really wanted to get this done. It's about the first tutorial I've written on something like this, so my aim is to hit the sweet spot between being verbose and not providing enough information.

Footnotes

1"List of tz database time zones - Wikipedia", Wikipedia, accessed July 14, 2018. https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
2"8.1 datetime - Basic date and time types - Python 2.7.18 documentation", Python.org, accessed July 14, 2018. https://docs.python.org/2/library/datetime.html.