Last year, I had to migrate my web pages very quickly to my own server because changes in my Institute’s website made it difficult for me to continue to host my web pages there. My old web site was set up more than two decades ago using raw html
, and it used php
for on-the-fly generation of content. Migration provided an excuse for me to move to static web pages for security and performance reasons. Since page generation happens locally, I can use any software that I find convenient without worrying about “hardening” it for security. Also the web server that I have in the cloud is relatively low on memory and processor resources, and therefore I would like to do as much of the processing locally as possible.
I tried both Jekyll and Nikola, and chose Nikola for two reasons. First, I found Nikola’s template engine, jinja2 to be more powerful. In particular, the ability to use any Python method in the template provides a lot of power. Second, and less important, I am more comfortable programming in Python
than in Ruby
.
Initially, I was put off by Nikola’s documentation which seems to be directed at the novice user, and seems to discourage the kind of user who wants full control over the whole process. The documentation nudges the user to choose some theme and then tweak it using a configuration file. The problem with this approach is that the “theme” controls not only the appearance but also the content because it contains all the templates. To exercise full control, it is necessary to write these templates myself; I would of course borrow liberally from templates written by more experienced and competent people, but I would not be satisfied without knowing what each template does.
Twice, I went back to Jekyll because its documentation is more friendly for a do-it-yourself user. But then, I persevered and was able to find what I wanted by reading the sections in the documentation on Nikola internals , Nikola template variables, Extending Nikola (plugins), then reading the template files inside the base-jinja
theme, and finally as a last resort, the Nikola source code and the Nikola plugins source code.
Minimal working example (MWE)
I now describe what I did to get a simple web site with just one page. Once, the minimal working example (MWE) is understood, it is easy to build on it and develop a full fledged website.
- Create a new site
nikola init -q new-website-nikola
cd new-website-nikola/
mkdir files/css
mkdir files/pages
- Create the single page in the website,
pages/index.md
, in markdown format with metadata inyaml
format. - Create the
css
file for the site,files/css/main.css
- Create a theme called
mytheme
manually without parent theme. The minimal theme consists of only three files.mytheme.theme
tellsNikola
that the theme uses thejinja
template engine.base.tmpl
is the base for all other templates. Its function is to (a) create anhtml
header with a link to the style filemain.css
, (b) set the title from the page/post title, (c) use the same title for the first level heading, and (d) insert the post contents (from a child template) in thehtml body
.page.tmpl
is the only child template in this minimal web site. It simply populates the contents of a web page for use in the parentbase.tmpl
.
- The steps for creating this minimal template are:
- Create the requisite folders with
mkdir -p themes/mytheme/templates
- Create
themes/mytheme/mytheme.theme
with following contents
[Theme]
engine = jinja
- Create
themes/mytheme/templates/base.tmpl
with the following contents.
{# -*- coding: utf-8 -*- #}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ title }}</title>
<link rel="stylesheet" href="/css/main.css">
</head>
<body>
<h1>{{ title }}</h1>
<section>
{% block content %}{% endblock %}
</section>
</body>
</html>
- Observe that this template obtains the title of the page using the per-page local variable
title
- Create
themes/mytheme/templates/page.tmpl
with the following contents
{# -*- coding: utf-8 -*- #}
{% extends 'base.tmpl' %}
{% block content %}
{{ post.text() }}
{% endblock %}
- Observe that this template obtains the text of the post by calling the text method:
post.text()
- Create a minimal
conf.py
which disables most plugins and builds only a bare bone web site: no blog, no gallery, no sitemap. The contents ofconf.py
are:
DEFAULT_LANG = "en"
THEME = "mytheme"
TIMEZONE = "Asia/Kolkata"
POSTS = ()
PAGES = (
("pages/*.rst", "", "page.tmpl"),
("pages/*.md", "", "page.tmpl"),
("pages/*.txt", "", "page.tmpl"),
("pages/*.html", "", "page.tmpl"),
)
INDEX_PATH = "blog"
COMPILERS = {
"markdown": ['.md', '.mdown', '.markdown'],
"html": ['.html', '.htm'],
}
METADATA_FORMAT = "YAML"
MARKDOWN_EXTENSIONS = [
'markdown.extensions.fenced_code',
'markdown.extensions.codehilite',
'markdown.extensions.extra']
PRETTY_URLS = False
# Plugins you don't want to use. Be careful 🙂
DISABLED_PLUGINS = ["redirect", "render_galleries", "render_listings",
"render_sources", "render_taxonomies", "robots",
"scale_images", "create_bundles", "sitemap"]
# Based on output of "nikola list", the ENABLED_PLUGINS are
# "copy_assets", "copy_files",
# "post_render",
# "render_pages", "render_posts", "render_site"
- We are now ready to build and test this site.
nikola build
nikola serve
Extending the minimal working example
Extending the MWE consists largely of (a) adding more content in the form of markdown files for each page in the website, and (b) adding more bells and whistles to the template files. It may also be necessary to add more templates for pages that we want to render differently from the base template. In the yaml
metadata of any page, we can add a line telling Nikola to use a different template. For example:
---
template: special.tmpl
---
If we just want to add some extra content while continuing to use the base template, we add something like the following in the body of the page:
{{% template %}}
{% include 'extra.tmpl' %}
{{% /template %}}
Unlike Jekyll
‘s Liquid
templates that work in normal markdown files, Nikola’s jinja2
works only in template files. The {{% template %}}
tag allows jinja2
code to be inserted inside a markdown file. Another useful technique uses shortcodes
. If we create a template file myfunction.tmpl
in the shortcodes
folder, we can call it like a function from a markdown file in the form {{% myfunction %}}
. Arguments can be passed to the myfunction
in two ways:
- In the post, we give the argument between
{{% myfunction %}}
and{{% /myfunction %}}
tags. In themyfunction.tmpl
shortcode, the argument is accessed as{{data}}
. - In the post, we pass named arguments and we access them in the shortcode using the same names. For example, the post contains
{{% myfunction message="Hello World!" %}}
. In the template, we can access the argument by its name{{ message }}
.
The complication that I encountered while using shortcodes was in accessing the post metadata. While post.meta('xyz')
worked in normal templates, it did not work in shortcodes where I had to use post.meta['en']['xyz']
(I discovered this syntax by reading the Nikola
source code).
Generating content from data files
In my website, my research papers are listed in different ways on different pages. In one page, only recent papers may be listed, while in another, all papers are listed. They may also be classified, sorted or formatted differently. The sensible way to manage this is to create a data file containing all the information about every paper and then write some code in say Python to process it in different ways.
The nice things is that jinja2
has for
loops and if
statements to loop through all records in a data file and then pick up only what is needed. Moreover, it is possible to call any Python method on any of the objects. This means that the Python
code that I was thinking about can actually be replicated in jinja2
In my case, the data is in yaml
files with each record having fields for title, year of publication, and other relevant information. These files have to be in the data
folder, and are referred to in the template using the file name. For example, data/books.yaml
contains data about all the books that I have written, and one of the fields is title
for the book title. In the template, I can list the titles of all the books using a for
loop
{% for book in data.books %}
<cite>{{book.title}}</cite>
{% endfor %}
Plugins
In my case, some content is more complex and not easy to generate using a template. It is much better to use a programming language to do this job. I therefore wrote a Nikola plugin which is basically python code that will be called by Nikola. The ConfigPlugin
category of plugin is used because Nikola scans for posts after this category of plugins has run. The my_generator
class extends the ConfigPlugin
class. Before rendering the site, Nikola will call my_generator.set_site
. This method generates and writes the extra files and then calls super(my_generator, self).set_site(site)
. This tells Nikola to scan the whole site again. Pages generated in this plugin will therefore be processed.
Blog
The last step was to migrate my blog to Nikola. This was a little weird because Nikola is designed for blogs, and normal web pages are a kind of afterthought. What I did on the other hand was to migrate all the web pages except the blog. Migrating the blog was a more complex and time consuming process that I will cover in a separate blog post.