Create a blog using Nikola static website generator

If you don’t know what is Nikola, it is a static website/blog generator (like gatsby and other tools). It’s written in Python and it is working out of the box for rendering markdown, rst, latex math formula and jupyter notebook files.

I like to understand what I am using, and pushing it to some limits to really get what I want from it, and making a blog with Nikola was no exception. Here I tried to summarize all the informations I found, and all the experimentation I did. I hope you’ll enjoy ! 🙏🏼

1) Installation

The first step is to have Python 3 installed on your computer, I recommend using virtual environment management.

Once you have create your virtual environment :

pip install --upgrade pip setuptools wheel
pip install --upgrade "Nikola[extras]"

2) Create the blog

After installing Nikola, creating your site will be very easy, just use the command nikola init <directory_name>. You can add the --demo argument if you want a website built with demo content.

All the configurations are done in a single conf.py file, at the root of your blog folder.

You can now build your site and see how it looks like. Use the command nikola auto to use a server with automatic rebuilds when changes is detected in your files. Visit http://128.0.0.1:8000 to see your site.

3) Add a Post

Now if you want to add a post in your blog you should use the command nikola new_post(the default use reStructuredText format, add -f markdown if like me you prefer to write in markdown). The CLI will ask for the title of your blog post and then create the file in the folder posts/*.md.

4) Enable Jupyter Notebook file format

Just add *.ipynb as recognizable formats:

POSTS = (
    ("posts/*.rst", "blog", "post.tmpl"),
    ("posts/*.md", "blog", "post.tmpl"),
    ("posts/*.txt", "blog", "post.tmpl"),
    ("posts/*.html", "blog", "post.tmpl"),
    ("posts/*.ipynb", "blog", "post.tmpl"), # new line
)
PAGES = (
    ("pages/*.rst", "", "page.tmpl"),
    ("pages/*.md", "", "page.tmpl"),
    ("pages/*.txt", "", "page.tmpl"),
    ("pages/*.html", "", "page.tmpl"),
    ("pages/*.ipynb", "", "page.tmpl"), # new line
)

You can create a blog post with nikola new_post -f ipynb or add your jupyter notebook in your posts folder. Don’t forget to add and configure these line in the metadata of your jupyter notebook file if you don’t let nikola create the file for yourself :

"nikola": {
   "category": "",
   "date": "2020-03-28 16:27:51 UTC+01:00",
   "description": "",
   "link": "",
   "slug": "jupyter-notebook-test",
   "tags": "",
   "title": "Jupyter Notebook Test",
   "type": "text"
  }

5) Using Markdown for your post

Nikola handle markdown files by default. The meta are auto generated when you use nikola new_post but I prefer to do it differently. Add the markdown.extensions.meta to your conf.py file.

MARKDOWN_EXTENSIONS =
 ['markdown.extensions.fenced_code',
  'markdown.extensions.codehilite',
  'markdown.extensions.extra',
  'markdown.extensions.meta']

Now you can simply add these line on top of your markdown files, in a pelican style, to indicate Nikola all the information it needs to build your post :

Title: Test post in markdown
Date: 2020-04-01
Slug: test-post
Tags: markdown, test
Categories: Tutorial

In my situation I decided to use pandoc instead of the default markdown compiler. I did this because I often have code blocks nested in numbered or bullet list and the default markdown compiler does not render those properly. It also looses the numbered list sometimes whereas pandoc is doing an absolute great job. Thanks to this great blog post that was explaining how to do it !

To use pandoc instead of the default markdown you need to first install it. You can use brew install pandoc if you have a mac, or look here for more instructions. Then you can change those lines in the conf.py :

COMPILERS = {
    "rest": ('.rst', '.txt'),
    # "markdown": ('.md', '.mdown', '.markdown'),
    "textile": ('.textile',),
    "txt2tags": ('.t2t',),
    "bbcode": ('.bb',),
    "wiki": ('.wiki',),
    "ipynb": ('.ipynb',),
    "html": ('.html', '.htm'),
    # PHP files are rendered the usual way (i.e. with the full templates).
    # The resulting files have .php extensions, making it possible to run
    # them without reconfiguring your server to recognize them.
    "php": ('.php',),
    # Pandoc detects the input from the source filename
    # but is disabled by default as it would conflict
    # with many of the others.
    "pandoc": ('.md', 'txt'),
}
...
PANDOC_OPTIONS = ['-f', 'gfm', '--toc', '-s']

See how I commented the markdown compiler and uncommented pandoc for .md files. The last two PANDOC_OPTIONS (–toc and -s) are used for automatically generating a Table of Content on the HTML generated output.

Once you use pandoc for compiling your markdown file, for creating a new blog post in markdown you need yo use this command nikola new _post -f pandoc and not markdown anymore.

Adding solely the pandoc as markdown compiler is unfortunately not enough because we lose the ability to use the CODE_COLOR_SCHEME = monokai option in conf.py. One solution is to use pandoc generated css for one of its code highlight theme (kate in my case).

Create a custom.css file in files/assets/css/ as explained here and add this css code :

code {white-space: pre-wrap;}
span.smallcaps {font-variant: small-caps;}
span.underline {text-decoration: underline;}
div.column {display: inline-block; vertical-align: top; width: 50%;}

a.sourceLine { display: inline-block; line-height: 1.25; }
a.sourceLine { pointer-events: none; color: inherit; text-decoration: inherit; }
a.sourceLine:empty { height: 1.2em; position: absolute; }
.sourceCode { overflow: visible; }
code.sourceCode { white-space: pre; position: relative; }
div.sourceCode { margin: 1em 0; }
pre.sourceCode { margin: 0; }
@media screen {
div.sourceCode { overflow: auto; }
}
@media print {
code.sourceCode { white-space: pre-wrap; }
a.sourceLine { text-indent: -1em; padding-left: 1em; }
}
pre.numberSource a.sourceLine
  { position: relative; }
pre.numberSource a.sourceLine:empty
  { position: absolute; }
pre.numberSource a.sourceLine::before
  { content: attr(data-line-number);
    position: absolute; left: -5em; text-align: right; vertical-align: baseline;
    border: none; pointer-events: all;
    -webkit-touch-callout: none; -webkit-user-select: none;
    -khtml-user-select: none; -moz-user-select: none;
    -ms-user-select: none; user-select: none;
    padding: 0 4px; width: 4em;
    background-color: #ffffff;
    color: #a0a0a0;
  }
pre.numberSource { margin-left: 3em; border-left: 1px solid #a0a0a0;  padding-left: 4px; }
div.sourceCode
  { color: #1f1c1b; background-color: #ffffff; }
@media screen {
a.sourceLine::before { text-decoration: underline; }
}
code span. { color: #1f1c1b; } /* Normal */
code span.al { color: #bf0303; background-color: #f7e6e6; font-weight: bold; } /* Alert */
code span.an { color: #ca60ca; } /* Annotation */
code span.at { color: #0057ae; } /* Attribute */
code span.bn { color: #b08000; } /* BaseN */
code span.bu { color: #644a9b; font-weight: bold; } /* BuiltIn */
code span.cf { color: #1f1c1b; font-weight: bold; } /* ControlFlow */
code span.ch { color: #924c9d; } /* Char */
code span.cn { color: #aa5500; } /* Constant */
code span.co { color: #898887; } /* Comment */
code span.cv { color: #0095ff; } /* CommentVar */
code span.do { color: #607880; } /* Documentation */
code span.dt { color: #0057ae; } /* DataType */
code span.dv { color: #b08000; } /* DecVal */
code span.er { color: #bf0303; text-decoration: underline; } /* Error */
code span.ex { color: #0095ff; font-weight: bold; } /* Extension */
code span.fl { color: #b08000; } /* Float */
code span.fu { color: #644a9b; } /* Function */
code span.im { color: #ff5500; } /* Import */
code span.in { color: #b08000; } /* Information */
code span.kw { color: #1f1c1b; font-weight: bold; } /* Keyword */
code span.op { color: #1f1c1b; } /* Operator */
code span.ot { color: #006e28; } /* Other */
code span.pp { color: #006e28; } /* Preprocessor */
code span.re { color: #0057ae; background-color: #e0e9f8; } /* RegionMarker */
code span.sc { color: #3daee9; } /* SpecialChar */
code span.ss { color: #ff5500; } /* SpecialString */
code span.st { color: #bf0303; } /* String */
code span.va { color: #0057ae; } /* Variable */
code span.vs { color: #bf0303; } /* VerbatimString */
code span.wa { color: #bf0303; } /* Warning */

Now your code blocks should be highlighted. It’s up to you to custom it further to change the color background or stuff like this.

I also wanted to style the table of content :

.p-summary.entry-summary #TOC {
  display: None; /* disable showing the TOC in my blog home page teasers */
}

#TOC {
  background-color: #e9f8f8;
  border-radius: 3px;
  padding: 18px 0px 1px 6px;
  margin-bottom: 20px;
}

6) Pages vs Posts

Nikola has two type for entries on your website, POSTS and PAGES.

POSTS

These are your blog posts. POSTS are added to feeds, indexes, tag lists and archives.

PAGES

These are generally static pages that may be built when you design your website. Once your design will be done you should not been making many new pages.

For example in PAGES, I have the following pages:

  • Resume (html)
  • Cheatsheet (html)

7) Customizing the navigation bar

Customization of the navigation top bar is done, again, in the conf.py file.

NAVIGATION_LINKS = {
    DEFAULT_LANG: (
        ("/resume/", "Resume"),
        ("/cheatsheet/", "Cheatsheet"),
        ("/archive/", "Archive"),
    )
}

This is an example of how I’ve done mine.

Nikola allows you to categorize posts in a number of ways such as category, tags, archives, and authors. For each means of categorizing, an associated index page is generated so that viewers can see all available posts (*_PAGES_ARE_INDEXES = True) or links associated to that category (_PAGES_ARE_INDEXES = False*).

You can choose for these indexes to produce a list of the full posts (or showing teasers instead of the full post) or a list of links to each post. Depending on your needs, you can change any of the following index settings in conf.py to True.

CATEGORY_PAGES_ARE_INDEXES = False
TAG_PAGES_ARE_INDEXES = False
ARCHIVES_ARE_INDEXES = False
AUTHOR_PAGES_ARE_INDEXES = False

This is what makes Nikola so customizable. For example, since there are less Categories, and you may have more posts under each category, you might want them as a list of links. Alternatively, with Tags there are usually more of them, so less posts under each Tag so you want a list of posts.

9) Enable a comment system

Because static sites do not have databases, you need to use a third-party comment system as documented on the official doc.

  1. Sign up for an account on https://disqus.com/.
  2. On Disqus, select “Create a new site” (or visit https://disqus.com/admin/create/).
  3. During configuration, take note on the “Shortname” you use. Other configs are not very important.
  4. At “Select a plan”, choosing the basic free plan is enough.
  5. At “Select Platform”, just skip the instructions. No need to insert the “Universal Code” manually, as it is built into Nikola. Keep all default and finish the configuration.

In conf.py, add your Disqus shortname:

COMMENT_SYSTEM = "disqus"
COMMENT_SYSTEM_ID = "[disqus-shortname]"

Deploy to GitHub and now the comment system should be enabled.

10) Deploying your website

My workflow is separated in two parts :

  1. Github Pages
  2. Netlify

Github Pages

I decided to host my blog files on GitHub and use their free service, GitHub Pages, for deploying my blog on this address https://mattioo.github.io.

For doing that you will need to have a GitHub account, and enable GitHub Pages. Once you created your repository as explained for GitHub Pages, initialize GitHub in your source directory

git init .
git remote add origin https://github.com/<USER_NAME>/<USER_NAME>.github.io

The conf.py should have the following settings.

GITHUB_SOURCE_BRANCH = 'src'
GITHUB_DEPLOY_BRANCH = 'master'
GITHUB_REMOTE_NAME = 'origin'
GITHUB_COMMIT_SOURCE = True

Create a .gitignore file with the following entries as a minimum. You may use gitignore.io to generate a suitable set of .gitignore entries for your platform by typing in the relevant tags (e.g., mac, nikola, jupyternotebooks).

cache
.doit.db
__pycache__
output
ipynb_checkpoints
*/.ipynb_checkpoints/*

By using the nikola github_deploy command, it will create a src branch that will contain your contents (i.e., *.ipynb*.md, and a master branch that will only contain your html output pages that are viewed by the browser.

nikola github_deploy

Netlify extra steps

Because of all these reasons I wanted to use Netlify for deploying my blog with a custom domains, www.brainsorting.com. I simply configured a trigger on Netlify to start building my blog when it detects any new push on my GitHub blog repository. It is as simple as said, everything kind of works out of the box and the service provided by Netlify has been very stable and giving me very good SEO statistics.

11) Archives

Nikola has many options for how you would display your archive of posts. I’ve kept it pretty simple on my end.

# Create per-month archives instead of per-year
CREATE_MONTHLY_ARCHIVE = False
# Create one large archive instead of per-year
CREATE_SINGLE_ARCHIVE = False
# Create year, month, and day archives each with a (long) list of posts
# (overrides both CREATE_MONTHLY_ARCHIVE and CREATE_SINGLE_ARCHIVE)
CREATE_FULL_ARCHIVES = False
# If monthly archives or full archives are created, adds also one archive per day
CREATE_DAILY_ARCHIVE = False
# Create previous, up, next navigation links for archives
CREATE_ARCHIVE_NAVIGATION = False
ARCHIVE_PATH = "archive"
ARCHIVE_FILENAME = "archive.html"

I use the recommended license :

LICENSE = """
<a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">
<img alt="Creative Commons License BY-NC-SA"
style="border-width:0; margin-bottom:12px;"
src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png"></a>"""

You might want to have a specific message (i.e., license, copyright, contact e-mail address) at the footer of every page and this is where to do it. In this case I’ve added a Mailchimp link so that readers can subscribe to my page.

CONTENT_FOOTER = '''
<center>
''' + MAILCHIMP_SIGNUP + '''
<br>
Contents &copy; {date} <a href="mailto:{email}">{author}</a> <a href="https://dev.to/mattioo"><i class="fab fa-dev" title="mattioo's DEV Profile"></i> </a> - Powered by <a href="https://getnikola.com" rel="nofollow">Nikola</a> {license} - favicon <a href="https://www.flaticon.com/">FlatIcon</a>
</center>
<br>
'''

13) Rendering math equations

I have enabled KaTeX because its prettier with the $...$ syntax as thats more similar to LaTeX.

USE_KATEX = True
KATEX_AUTO_RENDER = """
delimiters: [
    {left: "$$", right: "$$", display: true},
    {left: "\\\\[", right: "\\\\]", display: true},
    {left: "\\\\begin{equation*}", right: "\\\\end{equation*}", display: true},
    {left: "$", right: "$", display: false},
    {left: "\\\\(", right: "\\\\)", display: false}
]
"""

14) Implementing Google tools

I’ve enabled Google search form to search in my site.

SEARCH_FORM = """
 <form method="get" action="https://www.google.com/search" class="form-inline my-2 my-lg-0" role="search">
 <div class="form-group">
 <input type="text" name="q" class="form-control mr-sm-2" placeholder="Search">
 </div>
 <button type="submit" class="btn btn-secondary my-2 my-sm-0">
    <i class="fas fa-search"></i></button>
 </button>
 <input type="hidden" name="sitesearch" value="%s">
 </form>
""" % SITE_URL

Google Analytics

Google Analytics can be added to the bottom of <body> to function.

BODY_END = """
<!-- Global Site Tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_TRACKING_ID"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', '<YOUR GOOGLE ANALYTICS IDENTIFIER>');
</script>
"""

15) Customizing your blog

Theme & Template customization

To create a new theme, we can use the following command which will create a new folder in themes called brainsorting. It is using the mako templating engine and the parent theme is bootstrap4. We don’t necessarily want to create a theme from scratch, so we base it off the bootstrap4 theme (or whatever theme you want) and make the adjustments that we want.

nikola theme --new=brainsorting --engine=mako --parent=bootstrap4

We can also copy over any templates from the parent theme where we want to make modifications by using the following command :

nikola theme --copy-template=base.tmpl

If you want to examine all the components of the parent theme (i.e., bootstrap4 in my case), the following command will give you the path to the parent theme for you to explore.

nikola theme -g bootstrap4

The full list of templates is shown below:

.
├── authors.tmpl
├── base_helper.tmpl
├── base.tmpl
├── gallery.tmpl
├── index_helper.tmpl
├── listing.tmpl
├── pagination_helper.tmpl
├── post.tmpl
├── tags.tmpl
└── ui_helper.tmpl

For example if you want to make the nav bar sticky at the top, so that when readers scroll downwards, they can still access the menu bar, you need to update the base.tmpl file as shown below with the command sticky-top. To get the base.tmpl file in your template folder use nikola theme --copy-template=base.tmpl.

<nav class="navbar navbar-expand-md sticky-top mb-4

Setting your favicon

Pick an icon and store it in the folder ‘/file/’, then edit the conf.py as follows :

FAVICONS = (
    ("icon", "/brain.png", "128x128"),
)

Tweaking the CSS

This is quite easy and can be done by dropping a custom.css document here files/assets/css/custom.css. This will be loaded from the <head> block of the site when built.

If you really want to change the pages radically, you will want to do a custom theme.

Files and Listings

This two folders are used to transfer any file or code file to the output folder (your generated website). By default, putting anything in the files folder will be available in the root of your website. Anything in listings or subfolder will be available in output/listings. This last folder allows user to view and download any code file you put in this.

Date formatting

You can customize how the timestamp are displayed on your blog posts.

DATE_FORMAT = 'yyyy-MMM-dd'
DATE_FANCINESS = 2

I like to set it like this to have a more human friendly reading with dates displayed like “3 months ago”. Hovering the date with the mouse display the exact date.

Using mailchimp for user to subscribe

Mailchimp allows you to run e-mail campaigns and contact subscribers when you have new content on your site. See Getting Started with Mailchimp for more detailed instructions.

After you created your account you can create your signup form and get a code that looks like this one. Create a MAILCHIMP_SIGNUP variable in conf.py and paste this code :

MAILCHIMP_SIGNUP = """
<!-- Begin Mailchimp Signup Form -->
<div id="mc_embed_signup">
<form action="<YOUR MAILCHIMP IDENTIFIER>" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate>
    <div id="mc_embed_signup_scroll">
    <label for="mce-EMAIL">Subscribe</label>
    <input type="email" value="" name="EMAIL" class="email" id="mce-EMAIL" placeholder="email" required>
    <!-- real people should not fill this in and expect good things - do not remove this or risk form bot signups-->
    <div style="position: absolute; left: -5000px;" aria-hidden="true"><input type="text" name="b_3ffb6593478debd1efe5bf3e7_e432d28210" tabindex="-1" value=""></div>
    <div class="clear"><input type="submit" value="Subscribe" name="subscribe" id="mc-embedded-subscribe" class="button"></div>
    </div>
</form>
</div>

<!--End mc_embed_signup-->
"""

Loading bar

To make use of the pace.js loading library I added this code in the base_helper.tmpl file :

# below the <title> in the base_helper.tmpl file

{% if use_pace %}
    <script src="/assets/js/pace.min.js"></script>
    <link href="/assets/css/pace.css" rel="stylesheet" />
{% endif %}

I then activate it with in the conf.py settings :

GLOBAL_CONTEXT = {
    "use_pace": True,
}

Remove .html suffix from archive.html

This is just a small annoyance, but by default, the archive is located in /archive.html. If you want it to be in /archive/, add the following lines to your conf.py:

ARCHIVE_PATH = "archive"
ARCHIVE_FILENAME = "index.html"

Remember to also fix the navigation links:

NAVIGATION_LINKS = {
    DEFAULT_LANG: (
        ("/pages/resume/", "My Resume"),
        ("/pages/cheatsheet/", "Cheat Sheet"),
        ("/archive/", "Archive"),
    ),
}

Short blog post teaser in index page

The index.tmpl will generate a list of posts associated to the tag/category/year/author. This index can either be the entire post or post with just a teaser. To just show a teaser of the post, set conf.py as follows:

INDEX_TEASERS = True

Don’t forget to write in your post file where is the end of the teaser, in Markdown or html or ipynb do like this :

<!-- TEASER_END -->

In reStructuredText, select the end of your teasers with:

.. TEASER_END

If you are using teasers, the default is a Read more… link to access the full post. To make it more informative, you can have statements such as XX minute read… in conf.py as shown below.

INDEX_READ_MORE_LINK = '<p class="more"><a href="{link}">{reading_time} minute read…</a></p>'
FEED_READ_MORE_LINK = '<p><a href="{link}">{read_more}…</a> ({min_remaining_read})</p>'

16) Optimizing your blog

Filters

I want to be sure that all the files on my blog are optimized (such as .html, .js, .jpeg, .png, .css) so I am making use of the filters functionality.

In my conf.py I have the following settings for FILTERS :

FILTERS = {
   ".html": ["filters.typogrify"],
   ".css": ["filters.yui_compressor"],
   ".jpg": ["jpegoptim --strip-all -m75 -v %s"],
   ".png": ["filters.optipng"]
}

For them to work I need to have typogify, yui_compressor, jpegoptim, and optipng installed on my machine. Those instructions are for mac using homebrew:

brew install yuicompressor
brew install optipng
brew install jpegoptim
poetry add typogrify # or "pip install typogrify" if you don't use poetry

Posting automatically on Medium

To publish your Nikola posts on Medium there is this plugin.

You just need to install it with this command :

nikola plugin -i medium

Add a medium.json file in your blog folder with a generated access token that you can get here :

{
    "TOKEN": "your_token_here",
}

Then simply add the metadata markdown : yes in the blog post you want to publish on medium and run the command :

nikola medium

Posting automatically on Dev.to

To publish your Nikola posts on Dev there is this plugin.

You just need to install it with this command :

nikola plugin -i devto

Add a devto.json file in your blog folder with a generated access token that you can get here :

{
    "TOKEN": "your_token_here",
}

Then simply add the metadata devto : yes in the blog post you want to publish on medium and run the command :

nikola devto

Other plugin I didn’t try

  • similarity : Find posts that are similar to the one being read. Requires install of Natural Language Processing packages such as gensim.

Conclusion

That’s it for the big tutorial. I hope I was precise enough for giving you all the tools to make a blog very much like you want it. Some part of that article were very inspire (if not copy/pasted for some stuff) from resources down below that helped me a LOT to understand everything I need to understand to reach a result that I like with my blog. My next step will be to automate as much things as possible, you’ll know it in a new article when I will have achieve it 😄

Google group to discuss about Nikola : https://groups.google.com/forum/#!forum/nikola-discuss

Resources

https://getnikola.com/handbook.html

https://getnikola.com/getting-started.html

https://nikola.readthedocs.io/en/latest/manual

https://jiaweizhuang.github.io/blog/nikola-guide/

http://www.jaakkoluttinen.fi/blog/how-to-blog-with-jupyter-ipython-notebook-and-nikola/

https://randlow.github.io/posts/python/create-nikola-coding-blog/

Themes

https://themes.getnikola.com/v7/mdl/

https://hackerthemes.com/bootstrap-themes/

https://bootswatch.com/

Comments

Comments powered by Disqus