Updated Jan 11, 2020

Follow this tutorial to make a static website with Python and Github Pages. It uses the Pelican static website generator. Hosted with Github pages using Cloudflare DNS and CDN.

I followed this procedure to setup the blog using Python and Pelican. https://fedoramagazine.org/make-github-pages-blog-with-pelican/

Create accounts

Make sure you have accounts at Github and Cloudflare.

Creating repos on Github

You need to create 2 repos for your source files and your output files. The source files can be written in Markdown. Then you use a command to convert these source files into output HTML files.

For source files:


For output files:


Setup virtualenv

pip install virtualenv

Test your installation with:

virtualenv --version

Create a directory such as:

mkdir blog

cd blog

Create the environment with:

virtualenv env

Activate it with:

source env/bin/activate

The prompt will change to:

env $

Follow the steps to install packages. If you want to leave the environment deactivate it with:


Install Pelican

Inside the environment, install Pelican:

env $ pip install pelican
env $ pip install Markdown
env $ pip install typogrify

Setup the source repo

env $ git clone https://github.com/youruser/youruser.github.io-src.git ghpages

env $ cd ghpages

Setup the output repo

env $ git submodule add https://github.com/youruser/youruser.github.io.git output

Run the Pelican setup:

env $ pelican-quickstart

Caching your Github user and password

Later on when we are ready to publish to Github. When you git push. It will ask for your Github user and password.

Everytime that you push it will ask for your credentials. Which can be annoying if you want to push a few times within a short period.

You can cache your Github credentials using the following setup.

Inside the Pelican root directory make sure you are on the right repo:

env $ git remote -v

This should give you the .io-src.git repo.

env $ git config --global credential.helper 'cache --timeout=3600'

This will cache your credentials for 1 hour.

Now do the same for the output repo.

env $ cd output

env $ git config --global credential.helper 'cache --timeout=3600'

env $ cd ..

Pelican Setup

Answer the questions like this:

  • Where to create the new site: Click Enter
  • URL: https://youruser.github.io
  • Generate Makefile: Yes
  • Autoreload & simpleHTTP: Yes
  • Upload mechanism: Select No for all except for Github, select Yes.
  • Is this your personal page: Yes

According to this tutorial:


If you get an error saying that the output directory already exists, that this is fine.

Development and Production

There are config files.

The development config file is called pelicanconf.py.

The production config file is called publishconf.py.

Update publishconf.py file

Open the file publishconf.py using your favorite editor, such as vim.

env $ vim publishconf.py

Add or edit this line so it’s set to False


Update pelicanconf.py file:

Open the file pelicanconf.py.

env $ vim pelicanconf.py

The default file usually looks like this:

# -*- coding: utf-8 -*- #
from __future__ import unicode_literals

AUTHOR = u'Your Name'
SITENAME = u'Your Site Title'

PATH = 'content'

TIMEZONE = 'America/New_York'


# Feed generation is usually not desired when developing

# Blogroll
LINKS = (('Pelican', 'http://getpelican.com/'),
         ('Python.org', 'http://python.org/'),
         ('Jinja2', 'http://jinja.pocoo.org/'),
         ('You can modify those links in your config file', '#'),)

# Social widget
SOCIAL = (('You can add links in your config file', '#'),
          ('Another social link', '#'),)

# Uncomment following line if you want document-relative URLs when developing

Create structure of the blog

By default Pelican is setup as a blog, where the home page shows a list of blog posts with their summary.

To setup content of type Pages. Create a directory inside content such as:

env $ mkdir content/pages

To setup images that you can use for Posts or Pages. Create a directory inside content such as:

env $ mkdir content/images

If you will use a custom domain. Create another directory like this:

env $ mkdir content/extra

To setup pagination. Edit pelicanconf.py and add this line:


To create the path to images and to CNAME add these lines to pelicanconf.py:

STATIC_PATHS = ['images', 'extra/CNAME']
EXTRA_PATH_METADATA = {'extra/CNAME': {'path': 'CNAME'},}

Create a CNAME file

If you have a custom domain, you need to create a CNAME file

env $ vim content/extra/CNAME

Add your domain such as:


Create a blog post

To create a workflow from draft to published you need to edit pelicanconf.py:

DEFAULT_METADATA = { 'status': 'draft', }

Create the blog posts inside content using markdown such as:

env $ vim content/awesome-blog-post.md

Then use this format:

Title: Awesome Blog Post Title
Date: 2017-01-01 15:00
Category: Awesome category
Tags: tag1, tag2, tag3
Slug: awesome-blog-post
Author: Homer Simpson
Status: draft
Summary: Summary of this awesome blog post. This will be the SEO description

Start your awesome blog post here. Blabla

Build the html and run the server

Save the blog post and run this:

env $ make html && make serve

Open the browser in localhost:8000.

Since you set the status of the blog post to draft. You won’t see it on the home page. You would have to open it at the slug location such as:


To stop the server use Ctrl+C.

The draft to published workflow works well if you want others to review your content without having these posts appear on the home page. Otherwise just set the Status to published.

Create a blog post from a template

It’s pretty hard to remember the header for each blog post. I created a markdown template called new_post_template.md in my working directory and added this to .gitignore. Then I create a new blog post by copying this template such as:

$ cp new_post_template.md content/name_of_new_blogpost.md

The template has this content:

Title: Add Title
Date: 2019-04-18 20:00
Category: Linux
Tags: tag1, tag2
Slug: title-seo-with-dashes
Author: Tom Ordonez
Status: published
Summary: Short SEO summary.

Add content here.

Create a page

You can create static pages such as the typical about page.

This type of content has to go inside the content/pages directory.

env $ vim content/pages/about.md

Then follow a similar markdown format and status.

Title: About
Author: Your name
Status: published
Summary: awesome summary

Start writing your awesome page here.

Markdown crash course

Use this notation for hyperlinks:

[square bracket for text](parenthesis for URL)

If you want to open the hyperlink in a new tab/window. This cannot be done with markdown. Instead you have the follow the html format:

<a href="awesome-url" target="_blank">Awesome text</a>

Use this notation to add images:

![exclamation point and bracket for text](/assets/images/image-file.jpg)

On the notation above you literally have to put {static}. Then the location of the image, which can be jpg or png.

Formatting code, use:

4 leading spaces or `backticks` for inline.

For headlines use # hashtags. For an h2 headline use ## 2 hashtags.

For bullet points use * stars for ul or numbers for ol.

Adding a theme

To see the location of default themes:

pelican-themes -lv

This might change depending on your setup but in my case I got this:


To get more themes go to this Pelican themes site.

Go to the repo for the one you like.

Git clone the repo. For example:

env $ (cd ~/Downloads/ && git clone https://awesome-repo)

Let’s say that the theme is called homer. The location will be ~/Downloads/homer/.

Install the theme using this:

env $  pelican-themes --install ~/Downloads/homer/ --verbose

What this will do is just copy the whole directory into your virtualenv:


Now you have to add something to the pelicanconf.py config file:

THEME = 'homer'

Depending on the theme there might be a requirement to specify the css file. In which case you would need to add something like this:

COLOR_SCHEME_CSS = 'awesome.css'

Troubleshooting: gitmodules, multiple configurations found

You might get this error when pushing after making theme changes:

gitmodules, multiple configurations found for 'submodule.themes/name-of-theme.path'. Skipping second one!

Open your .gitmodules file. In my case it looked like this:

[submodule "themes/medius"]
path = themes/medius
url = https://github.com/onur/medius.git

[submodule "themes/pelican-alchemy"]
path = themes/pelican-alchemy
url = https://github.com/nairobilug/pelican-alchemy

My current theme is pelican-alchemy. I removed the first three lines with the other theme references.

Deploy to Github

Edit your .gitignore file and add this line:


Use this to generate the site:

env $ make publish

This will generate the site using the production settings. Which is the publishconf.py file.

This will also run the development config file because this file has this line:

from pelicanconf import *

Deploying to Github requires to git push both repos. The source and the output.

Deploy the output.

env $ cd output

env $ git add .

env $ git commit -m "Describe your commit here"

env $ git push -u origin master

This will ask for your Github credentials. They will be cached for 1 hour.

Now deploy the source.

env $ cd ..

env $ git add .

env $ git commit -m "Describe your commit here"

env $ git push -u origin master

Browse to you blog:


Custom Domain

This requires to make some changes to Github, your DNS provider and Cloudflare

Change the DNS settings where you have your website registered.

Create these records:

Type    Name               Content
ALIAS   yoursite.com       youruser.github.io
CNAME   www.yoursite.com   youruser.github.io
TXT     yoursite.com       youruser.github.io

Change your DNS name server to the Cloudflare ones:


Change the Github settings

Go to your output repo github.io.

Go to Settings.

Under Custom domain enter: www.yoursite.com


Change the Cloudflare settings

Add A records as seen here:


Verify with Google webmaster tools.

  • Add property
  • Add TXT google verification code

Thanks to this awesome post. This is how you setup Cloudflare.

Setup these Page rules.

Cache Level: Cache Everything

Forwarding URL: (Status Code: 301 - Permanent Redirect, URl: https://www.yoursite.com$1)

Always Use HTTPS

In your Overview dashboard make sure you have these settings (if you are on the free plan)

  • Security level: medium
  • SSL: Full
  • Caching level: Standard

Change the Pelican production config file

Inside publishconf.py change this setting from:

SITEURL = 'https://youruser.github.io'


SITEURL = 'https://www.yoursite.com'

Push Custom domain changes to Github

Deploy the output.

env $ make html && make publish

env $ cd output

env $ git add .

env $ git commit -m "Custom domain update"

env $ git push -u origin master

This might ask for your Github credentials. Unless they were cached.

Now deploy the source.

env $ cd ..

env $ git add .

env $ git commit -m "Custom domain update"

env $ git push -u origin master

Setup SEO: robots.txt, favicon.ico, sitemap.xml

Modify your pelicanconf.py file so it looks like this:


        'extra/CNAME': {'path': 'CNAME'},
        'extra/robots.txt': {'path': 'robots.txt'},
        'extra/favicon.ico': {'path': 'favicon.ico'},

### Plugins



# Sitemap
            'format': 'xml',
            'priorities': {
                'articles': 0.5,
                'indexes': 0.5,
                'pages': 0.5
            'changefreqs': {
                'articles': 'weekly',
                'indexes': 'daily',
                'pages': 'monthly'

Create the robots.txt

Go to your Pelican root directory and create the robots.txt file:

env $ vim content/extra/robots.txt

Add this line to the file:

User-agent: *

Create the favicon.ico

Go to the directory content/extra/ and add a favicon.ico image.

Create a directory for the plugins in the Pelican root directory:

env $ mkdir pelican-plugins

Create the sitemap

Create a directory for the sitemap plugin:

env $ mkdir pelican-plugins/sitemap

As seen on the Sitemap page. Copy the 3 files from this repo into the pelican-plugins/sitemap directory.

The directory should contain these files:


Generate the site:

env $ make publish

Verify that the sitemap.xml was generated inside the output directory.

Git Push SEO changes to Github

Deploy the output.

env $ cd output

env $ git add .

env $ git commit -m "sitemap, robots, favicon"

env $ git push -u origin master

This might ask for your Github credentials. Unless they were cached.

Now deploy the source.

env $ cd ..

env $ git add .

env $ git commit -m "sitemap, robots, favicon"

env $ git push -u origin master

Troubleshooting: using {filename} instead of {static}

I recently had issues with my blog when publishing posts. After calling make html && make publish. I started getting errors that I was using {filename} instead of {static}. My mistake. I had the incorrect syntax or perhaps just outdated in multiple posts, when linking to static images inside the content/images directory.

WARNING: {filename} used for linking to staticcontent

Following this solution to replace a word on multiple files, I ran this:

$ cd path/to/content folder
$ sed -i 's/filename/static/g' *.md

Like magic, it updated all the files with the correct syntax. I also had to run the same command inside content/pages for all .md files.

Fork and sync a local with a remote repo

As seen on my learning Github blog post. I have a setup to create blog posts on one machine and deploy the changes from a local server. I do this to experiment and learn with a sort of Git/Github lab.

The local server has installed the static web server in production, following all the steps as mentioned before. It’s linked to a sort of master Github account:

Master repo: https://github.com/original_owner/original_repo.git

The development machine has a forked repo from the master repo and it uses a different Github account.

Forked repo: https://github.com/your_username/your_fork.git

Configure a remote for a fork as seen here

Review the remotes you have:

$ git remove -v
> origin  https://github.com/your_username/your_fork.git (fetch)
> origin  https://github.com/your_username/your_fork.git (push)

Create a remote upstream repo with the original

$ git remote add upstream https://github.com/original_owner/original_repo.git

Check that the new remote was created

$ git remote -v

Sync your fork with the original upstream as seen here

$ git fetch upstream

It creates a new branch that stores master into upstream/master:

>  * [new branch]      master     -> upstream/master

Checkout the local master branch:

$ git checkout master

Merge the changes from upstream/master into local master:

$ git merge upstream/master

Troubleshooting: Merging is not possible because you have unmerged files

When running:

$ git merge upstream/master

You might get this error:

error: Merging is not possible because you have unmerged files.                                                                                                              
hint: Fix them up in the work tree, and then use 'git add/rm <file>'                                                                                                         
hint: as appropriate to mark resolution and make a commit.                                                                                                                   
fatal: Exiting because of an unresolved conflict.


$ git status

I had these:

Unmerged paths:
  (use "git add <file>..." to mark resolution)
        both modified:   .gitignore

Untracked files:
  (use "git add <file>..." to include in what will be committed)

I added these files and committed the changes from the git fetch upstream

$ git add .
$ git commit "merged upstream/master"