Building a site using Nikola#

Building a personal website is something that shows up on my personal todo/ideas list from time to time. My main motivation for doing so is to use it as a means to learn. I find that writing, and in particular explaining things, is an excellent way to structure one’s thoughts and learn things. On a practical level I hope to get a better understanding of all the technical aspects involved in building a website. This post describes the technical steps I took to build this site and is intended to serve as a future reference to myself or to others interested in setting up a personal site using Nikola.

To keep setting up a website manageable my approach was to do it in small steps. The first step was to decide on a framework and set up an empty (demo) website using that framework. I decided on using Nikola for this website. The next step was to add content to the website. I added posts that I wrote for the QuTech blog as well as some paper announcements. Adding old content made me familiar with some of the details of reStructuredText such as using footnotes, equations and adding images. With some content added to the site it was possible to focus on the looks of the site. Changing the look of the site corresponds to creating a theme. I have decided to use the lanyon theme with some tweaks. In the future I will add more features and pages (such as a proper about page).


Although this website was originally built using Nikola, I have migrated to a Sphinx + ablog configuration in early 2023. The website as you see it now was not created using Nikola.

Because this is a rather long and technical post there is a table of contents in the sidebar.

Choice of framework#

One particular important choice is the framework one wants to use. There were several things that affected my decision to build the website in Nikola, a static site generator build in python.

  • I want to be able to get something simple working with little effort yet be able to gradually add more complex features.

  • I need to be able to add both blog posts and regular web pages.

  • I want to have all my posts and the website itself under GIT version control.

  • I want to use an open source and free framework.

  • I would like it to be in python.

  • I want to have access to my data (posts etc.) and be able to move to another platform.

  • I want to write my posts in markdown or something similar.

  • It needs to support Latex equations.

  • It would be nice if it supports Jupyter notebooks.

Meshlogic has considered a very similar set of requirements and provided a nice summary on why he ended up using Nikola. In the end I considered several alternatives (Hugo, Nikola and Pelican) and decided on Nikola based on the native integration of Jupyter notebooks and the fact that it is written in python.

Setting up a Nikola blog#

After installing Nikola an empty site can be created using:

nikola init --demo mysite

The --demo argument adds demo files to the website. New posts can be created using nikola new_post. The website can be build and served using:

nikola build
nikola serve

The website can then be previewed on http://localhost:8000/. A much nicer way of serving the website is using:

nikola auto

This will automatically reload the website whenever changes are made.

Putting content on the site#

To be able to test the website I copied over my posts of the QuTech blog and added a few paper announcements. At a later point in time I will also add other pages like an about page and a publication page.

I have written the posts in reStructuredText or .rST because this is the default format, slightly more powerful than markdown, and used in a lot of documentation for python.

Nikola .rst tips#

Create a post using the nikola new_post command, this ensures the meta data is prefilled in correctly and the file is located in the right location.

  • Inline code: :code: my_command creates my_command.

  • Code blocks:

    1def hello_world():
    2    """
    3    A function that prints "Hello world!"
    4    """
    5    print('Hello world!')

    Can be created using :

    .. code-block:: python
        def hello_world():
            A function that prints "Hello world!"
            print('Hello world!')
  • Image files added to the /images/ folder can be added to the website using

    .. figure:: /images/my_figure.png
       :width: 969
       :align: center
       :figclass: thumbnail
  • A “Read more…” link on the main page can be added to posts by adding .. TEASER_END surrounded by two empty lines. To make this feature work, index teasers must be set to True in

    # Show teasers (instead of full posts) in indexes? Defaults to False.
  • Sections and subsections.

    This is a section title <h1>
    This is a subsection title <h2>
    This is a subsubsection title <h3>
  • Adding a link.

    `link_variable_name <http://>`_
  • To add equation support add the following to the file:

    # Do you want a add a Mathjax config file?
    <script type="text/x-mathjax-config">
        tex2jax: {
            inlineMath: [ ['$','$'], ["\\\(","\\\)"] ],
            displayMath: [ ['$$','$$'], ["\\\[","\\\]"] ],
            processEscapes: true
        displayAlign: 'center', // Change this to 'left' if you want left-aligned equations.
        "HTML-CSS": {
            styles: {'.MathJax_Display': {"margin": 0}}

    Make sure to add .. has_math: true to the metadata of your post. Equations can now be added using single $ signs for inline equations and double $$ signs for displayed equations: $E=mc^2$ creates $E=mc^2$.

  • References and footnotes.

    Some text containing numbered footnotes [1]_ , [#0]_, [#my_note]_ that are referenced directly below.
    .. [1] A footnote with an explicit number.
    .. [#0] This footnote is auto numbered.
    .. [#my_note] And this should be numbered to 3.

    Some text containing numbered footnotes [1] , [2], [3] that are referenced directly below.

    I find that using footnotes like this is also the easiest way to add citations/references to posts. Some kind of bibtex like auto bibliography is obviously preferred but not currently supported.

Setting up web hosting#

Setting up web hosting was relatively simple. As Nikola generates a static site you only need a place to host static html pages. Both github and gitlab support this for free. As I am using gitlab to host the repository I followed their tutorial to set it up. Because there is no example for Nikola I will share my .gitlab-ci.yaml file


  - pip3 install --upgrade pip
  - pip3 install -r requirements.txt
  - nikola build
    - public
  - master

    - pip3 install --upgrade pip
    - pip3 install -r requirements.txt
    - nikola build
    - public
  - master

Currently the build file uses a prebuilt image containing the nikola dependencies. It is possible that at some point I need to add more to my build script to build for example Jupyter notebooks that require more advanced features.

Because gitlab uses the /public/ folder as the page to host be sure to change the OUTPUT_FOLDER in

# Where the output site should be located
# If you don't use an absolute path, it will be considered as relative
# to the location of
OUTPUT_FOLDER = 'public'

I have purchased my domain name on hostnet and found their prices reasonable and am happy with their service. Getting the https security certificates set up for gitlab can take a few days so don’t worry when it initially tells you that your connection is insecure.

Modifying the design of the site#

After getting the basic site up and running I found that tweaking the appearance of the site was quite a bit more complicated than I expected, probably due to my lack of experience in web development. The Nikola Handbook does a good job of explaining all the options and individual components but even after scanning through it I was still confused.

The main thing that I was missing was an overview of how the website gets build and what gets determined where.

There are three things that determine how your site is build.

  1. The content of the website, specified in .rST, .md and .ipynb.
    • The files are located in the /posts/ and /pages/ folder.

    • Optional extra files are located in /images/, /files/ and /listings/,

  2. The theme consisting of two parts
    • assets containing css, javascript and images (such as icons)

    • templates that determine how the content gets translated into html pages. The templates are written in either Mako or Jinja depending on the theme.

  1. The settings specified in These are documented in detail in the Nikola Handbook.

Although building a template from scratch is a very bad idea, I found that understanding how to make a template helped me a lot in understanding how to edit an existing template. I recommend everyone to follow the creating a theme tutorial which covers all the basics. The tutorial covers how to covert the Lanyon theme from Jekyll to Nikola and in the progress teaches a lot about the inner workings of the template files, css files and how to read the output html files.

After following this tutorial my site still had a few issues so in the end I installed the theme using nikola theme -i lanyon. This copies all the files of the theme to the themes/lanyon/ folder. I find it helpful to also install another theme (such as the default bootstrap theme) nikola theme -i bootstrap to be able to look at the files as an example. The active theme is set in using THEME = "lanyon".

To edit a specific template file we use nikola theme -c "template_name.tmpl" to copy a build-in template to the current theme where it can be edited.

Tweaks to the lanyon theme#

There are several tweaks I have made to the lanyon theme to make it to my liking.

Custom about text in the sidebar#

The text displayed in the sidebar is now using the value of BLOG_DESCRIPTION = "my description" from by changing the contents of the sidebar-item class in base.tmpl to:

<!-- Toggleable sidebar -->
    <div class="sidebar" id="sidebar">
        <div class="sidebar-item">

Custom css for the tags#

This makes the lanyon theme use nice buttons for the tags. In themes/lanyon/assets/css/lanyon.css add:

.tags {
    padding-left: 0;
    margin-left: -5px;
    list-style: none;
    text-align: center;


.tags > li {
    display: inline-block;
    min-width: 10px;
    padding: 3px 7px;
    font-size: 12px;
    font-weight: bold;
    line-height: 1;
    color: #fff;
    text-align: center;
    white-space: nowrap;
    vertical-align: baseline;
    background-color: #999;
    border-radius: 10px;

.tags > li a {
    color: #fff;

Create a new file themes/lanyon/templates/tags.tmpl containing:

## -*- coding: utf-8 -*-
<%inherit file="base.tmpl"/>
<%namespace name="feeds_translations" file="feeds_translations_helper.tmpl" import="*"/>

<%block name="extra_head">
    ${feeds_translations.head(kind=kind, feeds=False)}

<%block name="content">
<article class="tagindex">
        <div class="metadata">
    % if cat_items:
        % if items:
        % endif
        % for text, full_name, path, link, indent_levels, indent_change_before, indent_change_after in cat_hierarchy:
            % for i in range(indent_change_before):
                <ul class="tags">
            % endfor
            <li><a class="reference badge"  href="${link}">${text}</a>
            % if indent_change_after <= 0:
            % endif
            % for i in range(-indent_change_after):
                % if i + 1 < len(indent_levels):
                % endif
            % endfor
        % endfor
        % if items:
        % endif
    % if items:
        <ul class="tags">
        % for text, link in items:
            % if text not in hidden_tags:
                <li><a class="reference badge"  href="${link}">${text|h}</a></li>
            % endif
        % endfor
    % endif

Prettier code boxes#

I use a custom code.css file that should be placed in themes/lanyon/assets/css/code.css.

**  Miki_Dark
**  - Inspired by Monokai, Solarized, Synthcity
**  - Style both RST and IPYNB posts
/* Background */
.code, .highlight, .highlight pre {
  background-color: #343434 !important;
  color: #e0e0e0 !important;

/* Text Selection */
.code::selection, .code *::selection, .highlight::selection, .highlight *::selection {
  background-color:#4060a0 !important;
  color: #eee !important;
.code::-moz-selection, .code *::-moz-selection, .highlight::-moz-selection, .highlight *::-moz-selection {
  background-color:#4060a0 !important;
  color: #eee !important;

.code .err, .highlight .err { background-color: #1e0010; color: #960050 }  /* Error */

.code .c  , .highlight .c   { color: #6EA06E; font-style: italic }  /* Comment */
.code .ch , .highlight .ch  { color: #6EA06E; font-style: italic }  /* Comment.Hashbang */
.code .cm , .highlight .cm  { color: #6EA06E; font-style: italic }  /* Comment.Multiline */
.code .cp , .highlight .cp  { color: #6EA06E; font-style: italic }  /* Comment.Preproc */
.code .cpf, .highlight .cpf { color: #6EA06E; font-style: italic }  /* Comment.PreprocFile */
.code .c1 , .highlight .c1  { color: #6EA06E; font-style: italic }  /* Comment.Single */
.code .cs , .highlight .cs  { color: #6EA06E; font-style: italic }  /* Comment.Special */

.code .n  , .highlight .n   { color: #e0e0e0 }  /* Name */
.code .w  , .highlight .w   { color: #e0e0e0 }  /* Text.Whitespace */
.code .p  , .highlight .p   { color: #839496 }  /* Punctuation */
.code .o  , .highlight .o   { color: #839496 }  /* Operator */

.code .k  , .highlight .k   { color: #78B6F0; font-weight: bold }  /* Keyword */
.code .kd , .highlight .kd  { color: #78B6F0; font-weight: bold }  /* Keyword.Declaration */
.code .kp , .highlight .kp  { color: #78B6F0; font-weight: bold }  /* Keyword.Pseudo */
.code .kr , .highlight .kr  { color: #78B6F0; font-weight: bold }  /* Keyword.Reserved */
.code .kt , .highlight .kt  { color: #78B6F0; font-weight: bold }  /* Keyword.Type */
.code .ow , .highlight .ow  { color: #78B6F0; font-weight: bold }  /* Operator.Word */

.code .kn , .highlight .kn  { color: #BB7AC8; font-weight: bold }  /* Keyword.Namespace */
.code .kc , .highlight .kc  { color: #BB7AC8; font-weight: normal} /* Keyword.Constant (None) */
.code .bp , .highlight .bp  { color: #BB7AC8; font-weight: normal} /* Builtin.Pseudo (True/False) */

.code .l  , .highlight .l   { color: #eb8798 }  /* Literal */
.code .m  , .highlight .m   { color: #eb8798 }  /* Literal.Number */
.code .mb , .highlight .mb  { color: #eb8798 }  /* Literal.Number.Bin */
.code .mf , .highlight .mf  { color: #eb8798 }  /* Literal.Number.Float */
.code .mh , .highlight .mh  { color: #eb8798 }  /* Literal.Number.Hex */
.code .mi , .highlight .mi  { color: #eb8798 }  /* Literal.Number.Integer */
.code .il , .highlight .il  { color: #eb8798 }  /* Literal.Number.Integer.Long */
.code .mo , .highlight .mo  { color: #eb8798 }  /* Literal.Number.Oct */

.code .s  , .highlight .s   { color: #e6db74 }  /* Literal.String */
.code .sa , .highlight .sa  { color: #e6db74 }  /* Literal.String.Affix */
.code .sb , .highlight .sb  { color: #e6db74 }  /* Literal.String.Backtick */
.code .sc , .highlight .sc  { color: #e6db74 }  /* Literal.String.Char */
.code .dl , .highlight .dl  { color: #e6db74 }  /* Literal.String.Delimiter */
.code .sd , .highlight .sd  { color: #e6db74 }  /* Literal.String.Doc */
.code .s2 , .highlight .s2  { color: #e6db74 }  /* Literal.String.Double */
.code .sh , .highlight .sh  { color: #e6db74 }  /* Literal.String.Heredoc */
.code .si , .highlight .si  { color: #e6db74 }  /* Literal.String.Interpol */
.code .sx , .highlight .sx  { color: #e6db74 }  /* Literal.String.Other */
.code .sr , .highlight .sr  { color: #e6db74 }  /* Literal.String.Regex */
.code .s1 , .highlight .s1  { color: #e6db74 }  /* Literal.String.Single */
.code .ss , .highlight .ss  { color: #e6db74 }  /* Literal.String.Symbol */
.code .ld , .highlight .ld  { color: #e6db74 }  /* Literal.Date */
.code .se , .highlight .se  { color: #e6db74 }  /* Literal.String.Escape */

.code .nc , .highlight .nc  { color: #cb4b16; font-weight: bold }  /* Name.Class */
.code .nf , .highlight .nf  { color: #F2872F }                     /* Name.Function */
.code .nb , .highlight .nb  { color: #a9dc76 }                     /* Name.Builtin (lib functions) */

.code .fm , .highlight .fm  { color: #a9dc76 }  /* Name.Function.Magic */
.code .vc , .highlight .vc  { color: #e0e0e0 }  /* Name.Variable.Class */
.code .vg , .highlight .vg  { color: #e0e0e0 }  /* Name.Variable.Global */
.code .vi , .highlight .vi  { color: #e0e0e0 }  /* Name.Variable.Instance */
.code .vm , .highlight .vm  { color: #BB7AC8 }  /* Name.Variable.Magic */

.code .nt , .highlight .nt  { color: #dc322f; font-weight: bold }  /* Name.Tag */
.code .na , .highlight .na  { color: #CE87DC; font-style: italic}  /* Name.Attribute */

.code .no , .highlight .no  { color: #BB7AC8 }  /* Name.Constant */
.code .nd , .highlight .nd  { color: #839496 }  /* Name.Decorator */
.code .ni , .highlight .ni  { color: #a9dc76 }  /* Name.Entity */
.code .ne , .highlight .ne  { color: #a9dc76 }  /* Name.Exception */
.code .nl , .highlight .nl  { color: #4EC9B0 }  /* Name.Label */
.code .nn , .highlight .nn  { color: #e0e0e0 }  /* Name.Namespace */
.code .nx , .highlight .nx  { color: #e0e0e0 }  /* Name.Other */
.code .py , .highlight .py  { color: #e0e0e0 }  /* Name.Property */
.code .nv , .highlight .nv  { color: #e0e0e0 }  /* Name.Variable */

.code .gp , .highlight .gp  { color: #888888 }  /* Generic.Prompt */
.code .go , .highlight .go  { color: #888888 }  /* Generic.Output */
.code .gd , .highlight .gd  { color: #f92672 }  /* Generic.Deleted */
.code .gi , .highlight .gi  { color: #a6e22e }  /* Generic.Inserted */
.code .gh , .highlight .gh  { color: #eab7b4 }  /* Generic.Heading */
.code .gu , .highlight .gu  { color: #eb8798 }  /* Generic.Subheading */
.code .ge , .highlight .ge  { font-style: italic}  /* Generic.Emph */
.code .gs , .highlight .gs  { font-weight: bold }  /* Generic.Strong */


.linenos {
  width: 3rem;

to lanyon.css to fix a small bug when using the linenos option for code boxes.

To fix the deault margins of the lanyon theme change the pre block in themes/lanyon/assets/css/poole.css to

pre {
      display: block;
      border-radius: 4px;
      padding: .6rem .5rem;
      font-size: .8rem;
      line-height: 1.4;
      white-space: pre;
      overflow: auto;
      word-break: break-all;
      word-wrap: break-word;
      background-color: #f9f9f9;

Post headers#

I wanted to remove information on the comments from the post headers so in both themes/lanyon/templates/index.tmpl and themes/lanyon/templates/post_header.tmpl I commented out the lines

if not post.meta('nocomments') and site_has_comments:
    <p class="commentline">${comments.comment_link(post.permalink(), post._base_path)}

Next steps#

I am fairly happy with the results although I do expect to continue to make small tweaks. Here is a list of small things I am not 100% happy with:

  • I don’t like how indented lists look.

  • The formatting of post titles can be tweaked a bit.

  • There is no about page explaining who I am and what the purpose of this site is.

  • There is missing javascript when I try to add e.g., folding boxes.

  • I want to add a list of my publications as a separate page.

Cool articles on building a Nikola blog#

These are links to some posts I looked at when created this website