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 :
white-space: pre-wrap;}
code {.smallcaps {font-variant: small-caps;}
span.underline {text-decoration: underline;}
span.column {display: inline-block; vertical-align: top; width: 50%;}
div
.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; }
a.sourceCode { overflow: visible; }
.sourceCode { white-space: pre; position: relative; }
code.sourceCode { margin: 1em 0; }
div.sourceCode { margin: 0; }
pre@media screen {
.sourceCode { overflow: auto; }
div
}@media print {
.sourceCode { white-space: pre-wrap; }
code.sourceLine { text-indent: -1em; padding-left: 1em; }
a
}.numberSource a.sourceLine
preposition: relative; }
{ .numberSource a.sourceLine:empty
preposition: absolute; }
{ .numberSource a.sourceLine::before
precontent: 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;
: none; -moz-user-select: none;
-khtml-user-select-ms-user-select: none; user-select: none;
padding: 0 4px; width: 4em;
background-color: #ffffff;
color: #a0a0a0;
}.numberSource { margin-left: 3em; border-left: 1px solid #a0a0a0; padding-left: 4px; }
pre.sourceCode
divcolor: #1f1c1b; background-color: #ffffff; }
{ @media screen {
.sourceLine::before { text-decoration: underline; }
a
}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 */ code span
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.
8) Indexes as a list of links or list of posts
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.
= False
CATEGORY_PAGES_ARE_INDEXES = False
TAG_PAGES_ARE_INDEXES = False
ARCHIVES_ARE_INDEXES = False AUTHOR_PAGES_ARE_INDEXES
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.
- Sign up for an account on https://disqus.com/.
- On Disqus, select “Create a new site” (or visit https://disqus.com/admin/create/).
- During configuration, take note on the “Shortname” you use. Other configs are not very important.
- At “Select a plan”, choosing the basic free plan is enough.
- 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:
= "disqus"
COMMENT_SYSTEM = "[disqus-shortname]" COMMENT_SYSTEM_ID
Deploy to GitHub and now the comment system should be enabled.
10) Deploying your website
My workflow is separated in two parts :
- Github Pages
- 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.
= 'src'
GITHUB_SOURCE_BRANCH = 'master'
GITHUB_DEPLOY_BRANCH = 'origin'
GITHUB_REMOTE_NAME = True GITHUB_COMMIT_SOURCE
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
= False
CREATE_MONTHLY_ARCHIVE # Create one large archive instead of per-year
= False
CREATE_SINGLE_ARCHIVE # Create year, month, and day archives each with a (long) list of posts
# (overrides both CREATE_MONTHLY_ARCHIVE and CREATE_SINGLE_ARCHIVE)
= False
CREATE_FULL_ARCHIVES # If monthly archives or full archives are created, adds also one archive per day
= False
CREATE_DAILY_ARCHIVE # Create previous, up, next navigation links for archives
= False
CREATE_ARCHIVE_NAVIGATION = "archive"
ARCHIVE_PATH = "archive.html" ARCHIVE_FILENAME
12) Content Footer
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 © {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.
= True
USE_KATEX = """
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
Google search
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.
= 'yyyy-MMM-dd'
DATE_FORMAT = 2 DATE_FANCINESS
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 :
<title> in the base_helper.tmpl file
# below the
{% 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"
ARCHIVE_PATH = "index.html" ARCHIVE_FILENAME
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:
= True INDEX_TEASERS
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.
= '<p class="more"><a href="{link}">{reading_time} minute read…</a></p>'
INDEX_READ_MORE_LINK = '<p><a href="{link}">{read_more}…</a> ({min_remaining_read})</p>' FEED_READ_MORE_LINK
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/
Comments
Comments powered by Disqus