Static site generator candidate software: Nikola

Contents

Nikola has this to say about itself on its [web page][N]:

  • Host anywhere. Static websites are safer, use fewer resources, and avoid vendor and platform lock-in. You can host a Nikola website on any web server, big or small. It’s just a bunch of HTML files and assets.
  • Fast rebuilds. Nikola is fast. We use doit, which provides incremental rebuilds—in other words, we rebuild only the pages that need rebuilding, saving CPU time, wall clock time and upload bandwidth.
  • Multiple input formats. Nikola will take input in many formats. Out of the box, we support reStructuredText, Markdown, IPython (Jupyter) Notebooks and HTML, and have plugins for many other formats.
  • Batteries included. Nikola comes with everything you need to build a modern website: blogs (with comments, tags, categories, archives, RSS/Atom feeds), multilingual support, easy image galleries, and code listings.
  • Easily extensible. Nikola is extensible. You can write a plugin to add any feature you want in a few lines of Python, or write your own theme in Mako or Jinja2. Or find something in the Plugin and Theme Indexes.
  • User-friendly CLI. Nikola has a friendly user interface that gets you up and running quickly and simplifies your work. You do not need to memorize headers just to create a post — we’ll write them for you.

Installation

[brian@sparrow ~]$ cd /opt
[brian@sparrow opt]$ virtualenv-3.4 nikola
Using base prefix '/opt/python'
New python executable in /var/local/opt-sparrow/nikola/bin/python3
Also creating executable in /var/local/opt-sparrow/nikola/bin/python
Installing setuptools, pip, wheel...
done.

[brian@sparrow tmp]$ cd nikloa
[brian@sparrow nikola]$ source bin/activate
Requirement already up-to-date: pip in ./lib/python3.7/site-packages (19.2.3)
Requirement already up-to-date: setuptools in ./lib/python3.7/site-packages (41.2.0)
Requirement already up-to-date: wheel in ./lib/python3.7/site-packages (0.33.6)

(nikola) [root@sparrow nikola]# pip install "Nikola[extras]" 2>&1 | tee 'pip.install.nikola-extras.text
Collecting Nikola[extras]
Collecting Yapsy>=1.11.223 (from Nikola[extras])
Collecting piexif>=1.0.3 (from Nikola[extras])
Collecting natsort>=3.5.2 (from Nikola[extras])
Collecting Pillow>=2.4.0 (from Nikola[extras])
Collecting doit>=0.30.1 (from Nikola[extras])
Collecting logbook>=1.3.0 (from Nikola[extras])
Collecting blinker>=1.3 (from Nikola[extras])
Collecting docutils>=0.13 (from Nikola[extras])
Collecting unidecode>=0.04.16 (from Nikola[extras])
Collecting PyRSS2Gen>=1.1 (from Nikola[extras])
Collecting requests>=2.2.0 (from Nikola[extras])
Collecting Markdown<3.0.0,>=2.4.0 (from Nikola[extras])
Collecting Pygments>=1.6 (from Nikola[extras])
Collecting mako>=1.0.0 (from Nikola[extras])
Collecting lxml>=3.3.5 (from Nikola[extras])
Collecting Babel>=2.6.0 (from Nikola[extras])
Collecting python-dateutil>=2.6.0 (from Nikola[extras])
Collecting notebook>=4.0.0; extra == "extras" (from Nikola[extras])
Collecting husl>=4.0.2; extra == "extras" (from Nikola[extras])
Collecting Jinja2>=2.7.2; extra == "extras" (from Nikola[extras])
Collecting aiohttp>=2.3.8; extra == "extras" (from Nikola[extras])
Collecting pygal>=2.0.0; extra == "extras" (from Nikola[extras])
Collecting typogrify>=2.0.4; extra == "extras" (from Nikola[extras])
Collecting micawber>=0.3.0; extra == "extras" (from Nikola[extras])
Collecting ruamel.yaml>=0.15; extra == "extras" (from Nikola[extras])
Collecting ghp-import2>=1.0.0; extra == "extras" (from Nikola[extras])
Collecting pyphen>=0.9.1; extra == "extras" (from Nikola[extras])
Collecting phpserialize>=1.3; extra == "extras" (from Nikola[extras])
Collecting watchdog>=0.8.3; extra == "extras" (from Nikola[extras])
Collecting ipykernel>=4.0.0; extra == "extras" (from Nikola[extras])
Collecting toml>=0.9.2; extra == "extras" (from Nikola[extras])
Collecting cloudpickle (from doit>=0.30.1->Nikola[extras])
Collecting pyinotify; sys_platform == "linux" (from doit>=0.30.1->Nikola[extras])
Collecting chardet<3.1.0,>=3.0.2 (from requests>=2.2.0->Nikola[extras])
Collecting idna<2.9,>=2.5 (from requests>=2.2.0->Nikola[extras])
Collecting certifi>=2017.4.17 (from requests>=2.2.0->Nikola[extras])
Collecting urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 (from requests>=2.2.0->Nikola[extras])
Collecting MarkupSafe>=0.9.2 (from mako>=1.0.0->Nikola[extras])
Collecting pytz>=2015.7 (from Babel>=2.6.0->Nikola[extras])
Collecting six>=1.5 (from python-dateutil>=2.6.0->Nikola[extras])
Collecting traitlets>=4.2.1 (from notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting nbconvert (from notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting terminado>=0.8.1 (from notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting tornado>=5.0 (from notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting jupyter-client>=5.3.1 (from notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting prometheus-client (from notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting jupyter-core>=4.4.0 (from notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting Send2Trash (from notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting pyzmq>=17 (from notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting nbformat (from notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting ipython-genutils (from notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting attrs>=17.3.0 (from aiohttp>=2.3.8; extra == "extras"->Nikola[extras])
Collecting yarl<2.0,>=1.0 (from aiohttp>=2.3.8; extra == "extras"->Nikola[extras])
Collecting multidict<5.0,>=4.5 (from aiohttp>=2.3.8; extra == "extras"->Nikola[extras])
Collecting async-timeout<4.0,>=3.0 (from aiohttp>=2.3.8; extra == "extras"->Nikola[extras])
Collecting smartypants>=1.8.3 (from typogrify>=2.0.4; extra == "extras"->Nikola[extras])
Collecting ruamel.yaml.clib>=0.1.2; platform_python_implementation == "CPython" and python_version < "3.8" (from ruamel.yaml>=0.15; extra == "extras"->Nikola[extras])
Collecting PyYAML>=3.10 (from watchdog>=0.8.3; extra == "extras"->Nikola[extras])
Collecting argh>=0.24.1 (from watchdog>=0.8.3; extra == "extras"->Nikola[extras])
Collecting pathtools>=0.1.1 (from watchdog>=0.8.3; extra == "extras"->Nikola[extras])
Collecting ipython>=5.0.0 (from ipykernel>=4.0.0; extra == "extras"->Nikola[extras])
Collecting decorator (from traitlets>=4.2.1->notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting testpath (from nbconvert->notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting entrypoints>=0.2.2 (from nbconvert->notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting bleach (from nbconvert->notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting defusedxml (from nbconvert->notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting mistune<2,>=0.8.1 (from nbconvert->notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting pandocfilters>=1.4.1 (from nbconvert->notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting ptyprocess; os_name != "nt" (from terminado>=0.8.1->notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting jsonschema!=2.5.0,>=2.4 (from nbformat->notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting jedi>=0.10 (from ipython>=5.0.0->ipykernel>=4.0.0; extra == "extras"->Nikola[extras])
Collecting pickleshare (from ipython>=5.0.0->ipykernel>=4.0.0; extra == "extras"->Nikola[extras])
Collecting prompt-toolkit<2.1.0,>=2.0.0 (from ipython>=5.0.0->ipykernel>=4.0.0; extra == "extras"->Nikola[extras])
Collecting backcall (from ipython>=5.0.0->ipykernel>=4.0.0; extra == "extras"->Nikola[extras])
Collecting pexpect; sys_platform != "win32" (from ipython>=5.0.0->ipykernel>=4.0.0; extra == "extras"->Nikola[extras])
Collecting webencodings (from bleach->nbconvert->notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting pyrsistent>=0.14.0 (from jsonschema!=2.5.0,>=2.4->nbformat->notebook>=4.0.0; extra == "extras"->Nikola[extras])
Collecting parso>=0.5.0 (from jedi>=0.10->ipython>=5.0.0->ipykernel>=4.0.0; extra == "extras"->Nikola[extras])
Collecting wcwidth (from prompt-toolkit<2.1.0,>=2.0.0->ipython>=5.0.0->ipykernel>=4.0.0; extra == "extras"->Nikola[extras])

Successfully built Yapsy logbook blinker PyRSS2Gen mako husl typogrify micawber
  phpserialize watchdog pyinotify tornado prometheus-client yarl PyYAML
  pathtools pandocfilters backcall pyrsistent

Installing collected packages: Yapsy, piexif, natsort, Pillow, cloudpickle,
  pyinotify, doit, logbook, blinker, docutils, unidecode, PyRSS2Gen, chardet,
  idna, certifi, urllib3, requests, Markdown, Pygments, MarkupSafe, mako, lxml,
  pytz, Babel, six, python-dateutil, decorator, ipython-genutils, traitlets,
  jupyter-core, pyzmq, tornado, jupyter-client, parso, jedi, pickleshare,
  wcwidth, prompt-toolkit, backcall, ptyprocess, pexpect, ipython, ipykernel,
  Jinja2, testpath, entrypoints, webencodings, bleach, defusedxml, pyrsistent,
  attrs, jsonschema, nbformat, mistune, pandocfilters, nbconvert, terminado,
  prometheus-client, Send2Trash, notebook, husl, multidict, yarl,
  async-timeout, aiohttp, pygal, smartypants, typogrify, micawber,
  ruamel.yaml.clib, ruamel.yaml, ghp-import2, pyphen, phpserialize, PyYAML,
  argh, pathtools, watchdog, toml, Nikola

Successfully installed Babel-2.7.0 Jinja2-2.10.3 Markdown-2.6.11
  MarkupSafe-1.1.1 Nikola-8.0.2 Pillow-6.2.0 PyRSS2Gen-1.1 PyYAML-5.1.2
  Pygments-2.4.2 Send2Trash-1.5.0 Yapsy-1.12.2 aiohttp-3.6.1 argh-0.26.2
  async-timeout-3.0.1 attrs-19.2.0 backcall-0.1.0 bleach-3.1.0 blinker-1.4
  certifi-2019.9.11 chardet-3.0.4 cloudpickle-1.2.2 decorator-4.4.0
  defusedxml-0.6.0 docutils-0.15.2 doit-0.31.1 entrypoints-0.3
  ghp-import2-1.0.1 husl-4.0.3 idna-2.8 ipykernel-5.1.2 ipython-7.8.0
  ipython-genutils-0.2.0 jedi-0.15.1 jsonschema-3.0.2 jupyter-client-5.3.3
  jupyter-core-4.5.0 logbook-1.5.2 lxml-4.4.1 mako-1.1.0 micawber-0.5.0
  mistune-0.8.4 multidict-4.5.2 natsort-6.0.0 nbconvert-5.6.0 nbformat-4.4.0
  notebook-6.0.1 pandocfilters-1.4.2 parso-0.5.1 pathtools-0.1.2 pexpect-4.7.0
  phpserialize-1.3 pickleshare-0.7.5 piexif-1.1.3 prometheus-client-0.7.1
  prompt-toolkit-2.0.10 ptyprocess-0.6.0 pygal-2.4.0 pyinotify-0.9.6
  pyphen-0.9.5 pyrsistent-0.15.4 python-dateutil-2.8.0 pytz-2019.2 pyzmq-18.1.0
  requests-2.22.0 ruamel.yaml-0.16.5 ruamel.yaml.clib-0.2.0 six-1.12.0
  smartypants-2.0.1 terminado-0.8.2 testpath-0.4.2 toml-0.10.0 tornado-6.0.3
  traitlets-4.3.3 typogrify-2.0.7 unidecode-1.1.1 urllib3-1.25.6 watchdog-0.9.0
  wcwidth-0.1.7 webencodings-0.5.1 yarl-1.3.0

Nikola commands

  • nikola auto - builds and serves a site; automatically detects site changes, rebuilds, and optionally refreshes a browser
  • nikola build - run tasks
  • nikola check - check links and files in the generated site
  • nikola clean - clean action / remove targets
  • nikola console - start an interactive Python console with access to your site
  • nikola default_config - Print the default Nikola configuration.
  • nikola deploy - deploy the site
  • nikola doit_auto - automatically execute tasks when a dependency changes
  • nikola dumpdb - dump dependency DB
  • nikola forget - clear successful run status from internal DB
  • nikola github_deploy - deploy the site to GitHub Pages
  • nikola help - show help
  • nikola ignore - ignore task (skip) on subsequent runs
  • nikola import_wordpress - import a WordPress dump
  • nikola info - show info about a task
  • nikola init - create a Nikola site in the specified folder
  • nikola list - list tasks from dodo file
  • nikola new_page - create a new page in the site
  • nikola new_post - create a new blog post or site page
  • nikola orphans - list all orphans
  • nikola plugin - manage plugins
  • nikola reset-dep - recompute and save the state of file dependencies without executing actions
  • nikola rst2html - compile reStructuredText to HTML files
  • nikola serve - start the test webserver
  • nikola status - display site status
  • nikola strace - use strace to list file_deps and targets
  • nikola subtheme - given a swatch name from bootswatch.com or hackerthemes.com and a parent theme, creates a custom theme
  • nikola tabcompletion - generate script for tab-completion
  • nikola theme - manage themes
  • nikola version - print the Nikola version number
  • nikola help - show help / reference
  • nikola help <command> - show command usage
  • nikola help <task-name> - show task usage

Creating a new blog

[brian@sparrow tmp]$ cd /var/tmp; source Nikola bin/activate
(Nikola) [brian@sparrow tmp]$ nikola init /var/tmp/Nikola-Text

Creating Nikola Site

This is Nikola v8.0.2.  We will now ask you a few easy questions about your new site.
If you do not want to answer and want to go with the defaults instead, simply restart with the `-q` parameter.
--- Questions about the site ---
Site title [My Nikola Site]: The DevOps Hobbit
Site author [Nikola Tesla]: Bradelgar Boffenridge
Site author's e-mail [n.tesla@example.com]: bradelgar@groupbcl.ca
Site description [This is a demo site for Nikola.]: Adventures of a Hobbit in DevOps
Site URL [https://example.com/]: https://groupbcl.ca/
Enable pretty URLs (/page/ instead of /page.html) that don't need web server configuration? [Y/n] 
--- Questions about languages and locales ---
We will now ask you to provide the list of languages you want to use.
Please list all the desired languages, comma-separated, using ISO 639-1 codes.
The first language will be used as the default.
Type '?' (a question mark, sans quotes) to list available languages.
Language(s) to use [en]: 

Please choose the correct time zone for your blog. Nikola uses the tz database.
You can find your time zone here:
https://en.wikipedia.org/wiki/List_of_tz_database_time_zones

Time zone [America/Winnipeg]: 
    Current time in America/Winnipeg: 02:21:11
Use this time zone? [Y/n] 
--- Questions about comments ---
You can configure comments now.  Type '?' (a question mark, sans quotes) to
list available comment systems.  If you do not want any comments, just
leave the field blank.
Comment system: ?

# Available comment systems:
#   disqus, facebook, intensedebate, isso, muut, commento

Comment system: 

That's it, Nikola is now configured.  Make sure to edit conf.py to your liking.
If you are looking for themes and addons, check out https://themes.getnikola.com/
and https://plugins.getnikola.com/.

Have fun!
[2019-09-09T07:21:39Z] INFO: init: Created empty site at /var/tmp/Nikola-Text.

(Nikola) [brian@sparrow tmp]$ cd Nikola-Test
(Nikola) [brian@sparrow Nikola-Test]$ nikola build
Scanning posts........done!
.  copy_assets:output/assets/css/bootblog.css
.  copy_assets:output/assets/js/bootstrap.min.js
.  copy_assets:output/assets/js/popper.min.js
.  copy_assets:output/assets/js/jquery.min.js
.  copy_assets:output/assets/css/bootstrap.min.css
.  copy_assets:output/assets/css/theme.css
.  copy_assets:output/assets/js/fancydates.min.js
.  copy_assets:output/assets/js/gallery.js
.  copy_assets:output/assets/js/html5shiv-printshiv.min.js
.  copy_assets:output/assets/js/gallery.min.js
.  copy_assets:output/assets/js/baguetteBox.min.js
.  copy_assets:output/assets/js/html5.js
.  copy_assets:output/assets/js/justified-layout.min.js
.  copy_assets:output/assets/js/moment-with-locales.min.js
.  copy_assets:output/assets/js/fancydates.js
.  copy_assets:output/assets/xml/rss.xsl
.  copy_assets:output/assets/xml/atom.xsl
.  copy_assets:output/assets/css/baguetteBox.min.css
.  copy_assets:output/assets/css/rst_base.css
.  copy_assets:output/assets/css/html4css1.css
.  copy_assets:output/assets/css/rst.css
.  copy_assets:output/assets/css/ipython.min.css
.  copy_assets:output/assets/css/nikola_rst.css
.  copy_assets:output/assets/css/nikola_ipython.css
.  copy_assets:output/assets/css/code.css
.  render_galleries:output/galleries
.  render_galleries:output/galleries/index.html
.  render_galleries:output/galleries/rss.xml
.  render_listings:output/listings/index.html
.  render_taxonomies:output/index.html
.  render_taxonomies:output/archive.html
.  render_taxonomies:output/categories/index.html
.  render_posts:timeline_changes
.  render_taxonomies:output/rss.xml
.  create_bundles:output/assets/css/all-nocdn.css
.  create_bundles:output/assets/css/all.css
.  create_bundles:output/assets/js/all-nocdn.js
.  create_bundles:output/assets/js/all.js
.  sitemap:output/sitemap.xml
.  sitemap:output/sitemapindex.xml
.  robots_file:output/robots.txt
(Nikola) [brian@sparrow Nikola-Text]$ 

Implementation notes

I made the following changes to the conf.py file:

  • Added 3 entries to NAVIGATION_LINKS:
    • ("/index.html", "Home"),
    • ("/about", "About Me"),
    • ("/blog", "Blog"),
  • In PAGES, changed the relative directory from pages to ""
  • Changed METADATA_FORMAT to YAML
  • Changed INDEX_PATH from "" to blog
  • Set up the following MARKDOWN_EXTENSIONS:
    • markdown.extensions.abbr
    • markdown.extensions.attr_list
    • markdown.extensions.toc
    • markdown.extensions.auc_headers
    • markdown.extensions.autoxref
    • markdown.extensions.cell_row_span
    • markdown.extensions.codehilite
    • markdown.extensions.def_list
    • markdown.extensions.extra
    • markdown.extensions.fenced_code
    • markdown.extensions.footnotes
    • markdown.extensions.linebreak_plus
    • markdown.extensions.meta
    • markdown.extensions.percent_comments
    • markdown.extensions.sane_lists
    • markdown.extensions.smart_strong
    • markdown.extensions.smarty
    • markdown.extensions.tables
    • markdown.extensions.urlize
    • markdown.extensions.unimoji

Tweaking the theme

I found the default bootstrap4 theme to be very functional, but there were a couple of annoyances I wanted to address:

  • I wanted an image of some sort at the very top of the page, an idea I got from Schneier on Security
  • For a bit of diversity, I wanted the heading to use a serif font
  • The large font in the blockquotes was a bit overwhelming

For the graphic, I spent a couple of hours searching the web for free (both no cost and no restrictions) images and settled on the one you see here. It’s actually a sightly rotated and heavily cropped version of the full image. To get the image into the glog I updated bootstrap4/templates/base.tmpl with an <img> tag, and put the image itself into the bootstrap4/assets/image directory.

For the presentation changes, I created $HOME/blog/files/assets/css/custom.css with the following content:

/* Use a serif font for the headings */
h1, h2, h3, h4, h5, h6 {
  font-family: "Playfair Display", Georgia, "Times New Roman", serif;
}

/* Put a light shaded backround on blockquotes and reduce the font size */
blockquote {
    padding: 1em;
    background: #ddd;
}
blockquote > p {
    font-size: 80%;
}

Tweaking the CSS for table gridlines

It appears the CSS for a lot of site generators removes gridlines from tables, possibly beccause they want to use tables for layout. But this conflicts with the way I want to do tables.

One way I could do this is to somehow mark tables in my source files with a CSS class indicating they should get gridlines. Unfortunately there’s no support in Python Markdown to declare a CSS class on a table, such as:

Heading 1 | Heading 2
:--------:| ---------   {.gridlines}
  1       | Apple
  2       | Bear

The solution was actually rather straigtforward: I added the following lines to custom.css:

/* Add gridlines to tables not of class .codehilitetable/.linenos/.code */
table:not(.codehilitetable) { border-collapse: collapse; border: 1px solid black; }
th { background-color: lightgrey; }
td:not(.linenos):not(.code), th { border: solid grey 1px; padding: 5px; }

The verdict: I like this software!

So far Nikola does pretty much everything I want for a static site generator. Between it and my mk-blog-content program I can seamlessly add content to my notebook files and upload the changes to my blog with very few commands.