You can also read that article on my blog or on medium !
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.
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.
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.
- 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:
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 :
- 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.
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.dev. 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"
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.
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
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.
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/
Top comments (0)