Optimizing your Nikola blog for Jupyter notebooks

You've installed Nikola. You've also followed some instructions on how to use Jupyter notebooks as posts. Everything works! But after you make your first Jupyter post, the results are... uninspiring. The input prompts are ugly. If you've written a post designed primarily for the content, not the code, there's no was to turn off showing the code cells. This post will describe how I -- someone with almost no experience with javascript, CSS, etc -- made my blog more functional to work with Jupyter notebooks.

Install Nikola

Follow the instructions on the Nikola website, or a million blog posts from other users.

Pick a theme

Pick the theme you like best and install with nikola theme -i theme_name. I used the bootstrap3 theme. These instructions should work for any Bootstrap-based theme, and possible others though I haven't tested them.

Create a new theme

Nikola allows for theme inheritance, so we'll create a new theme that inherits from bootstrap3. This means in our new theme we only have to create the files we want to change, otherwise Nikola will fall back to the parent theme. Following the official instructions:

nikola theme -n bootyper --parent bootstrap4 --engine mako

This will create a new directory /themes/bootyper which is empty except for a file called bootyper.theme. You'll also be prompted to update your current theme in your conf.py file. Might as well do that now. While you're in conf.py, update the following sections.

POSTS = (
    ("posts/*.rst", "posts", "post.tmpl"),
    ("posts/*.md", "posts", "post.tmpl"),
    ("posts/*.txt", "posts", "post.tmpl"),
    ("posts/*.html", "posts", "post.tmpl"),
    ("posts/*.ipynb", "posts", "post_ipynb.tmpl"),
)
PAGES = (
    ("pages/*.rst", "pages", "page.tmpl"),
    ("pages/*.md", "pages", "page.tmpl"),
    ("pages/*.txt", "pages", "page.tmpl"),
    ("pages/*.html", "pages", "page.tmpl"),
    ("pages/*.ipynb", "pages", "post_ipynb.tmpl"),
)

What we're doing here is telling Nikola that when you add a Jupyter notebook as a page or post, that you want to use a different template file called post_ipynb.tmpl.

Make a Jupyter template

Now let's make that template. Copy the post.tmpl file from your parent theme and rename it as so:

cp ./themes/bootstrap4/templates/post.tmpl ./themes/bootyper/templates/post_ipynb.tmpl

Now we're free to modify the new template file, and it will only apply to posts that use Jupyter notebooks. First let's add a custom CSS file to the header. The post template we're using provides us with a block of code called "extra_head" which will be inserted into the end of the <head> element when the HTML is rendered. Let's link to our custom CSS file here

<%block name="extra_head">
[...]
<link rel="stylesheet" href="../assets/css/bootyper.css">
</%block>

Clean up the Jupyter CSS

We need to create the file we just linked to. Open a new file ./themes/bootyper/assets/css/bootyper.css. This is where we'll stick our custom CSS that only applies to posts and pages made with Jupyter notebooks.

div.prompt {
    display: none;
}

div.input  {
    border: none;
    background-color: none;
}

div.input * {
    background-color: none;
}

/* set a max-width for horizontal fluid layout and make it centered */
.body-content {
  margin-right: auto;
  margin-left: auto;
  max-width: 750px; /* or 950px */
}

div.output_subarea {
    /* Widens image-containing divs so that image is full body width */
    max-width: 100%;
}

It's totally up to you how to style your notebook. I totally ditch the input prompts, but instead you could just make them prettier than the default. I also change the styling of the input code blocks, and make the content div a little narrower so it's easier to read on wider screens. Finally I make embedded images the full width of the content div. I'm not much of a designer so I stop here, but the possibilities are of course endless.

Going further with Javascript

The one thing I really wanted for my blog was to make it optional for the reader to see the raw code. Some posts would be designed as code-throughs, but others would be read mainly for the text and images and the code would be an extra for those interested. I've added two features to make this possible: 1) A button that toggles the visibility of the input code blocks, and 2) a metadata setting to set the default visibility of the code blocks on page load. Let's create a javascript library for these features in ./themes/bootyper/assets/js/bootyper.js.

function code_toggle() {
    if (!code_show){
    $('div.input').hide();
    //$('div.prompt.output_prompt').hide();  //unnecessary since I'm hiding all prompts anyways in css
    } else {
    $('div.input').show();
    }
 code_show = !code_show
}

This function checks the value of the javascript variable code_show and either shows or hides the div with the input code, then switches the value of code_show. We'll run this function on page load, and then when the reader clicks on a button to show/hids the code.

Now we need to make another update to our template file post_ipynb.tmpl:

<%block name="extra_head">
[...]
<link rel="stylesheet" href="../assets/css/bootyper.css">
<script type="text/javascript" src="https://code.jquery.com/jquery.min.js"></script>
<script type="text/javascript" src="../assets/js/bootyper.js"></script>

<script type="text/javascript">
code_show = false;
$( document ).ready(code_toggle);
</%block>

Here we've added a jquery library and our custom bootyper.js file. I've hard-coded in the default value of code_show as false, then I call the code_toggle function so that when the page loads the code cells aren't shown. Further down in the template file we can add a button to run this script:

<%block name="content">
<article class="post-${post.meta('type')} h-entry hentry postpage" itemscope="itemscope" itemtype="http://schema.org/Article">
    ${pheader.html_post_header()}


    <form id="toggle-button" action="javascript:code_toggle()"><input type="submit" class="btn btn-primary" value="Click here to toggle on/off the raw code."></form>
    [...]
</%block>

We're only adding the <form> element. I place the button immediately at the top of the content of the article, below the title and data/author. You can place it wherever you'd prefer, and give it any style you'd like. Again I just keep it simple and use a default bootstrap button style. And that's all it takes to have a button to optionally show or hide the input code in your Jupyter notebook!

One more step: Nikola metadata

Finally, I want to be able to set the default code_show value on a post by post basis. I do this using the notebook metadata. In Jupyter, go to Edit->Edit Notebook Metadata and you'll see see some JSON elements. By running nikola add_post -i notebook.ipynb to add your notebook as a Nikola post, Nikola will insert some custom fields into the JSON. We just need to add one field, code = "false", to the metadata so it looks something like:

{
  "kernelspec": {
    "name": "python3",
    "display_name": "Python 3",
    "language": "python"
  },
  "language_info": {
    "name": "python",
    "version": "3.6.6",
    "mimetype": "text/x-python",
    "codemirror_mode": {
      "name": "ipython",
      "version": 3
    },
    "pygments_lexer": "ipython3",
    "nbconvert_exporter": "python",
    "file_extension": ".py"
  },
  "nikola": {
    "category": "",
    "code": "false",
    "date": "2019-01-14 16:27:18 UTC-08:00",
    "description": "",
    "link": "",
    "slug": "optimizing-your-nikola-blog-for-jupyter-notebooks",
    "status": "draft",
    "tags": "",
    "title": "Optimizing your Nikola blog for Jupyter notebooks",
    "type": "text"
  }
}

These metadata fields will automoatically get passed to the Nikola template engine so they're available to use in your template file. Let's add an if/else statement in the template to set the code_show variable based on our code metadata field.

<%block name="extra_head">
[...]
<link rel="stylesheet" href="../assets/css/bootyper.css">
<script type="text/javascript" src="https://code.jquery.com/jquery.min.js"></script>
<script type="text/javascript" src="../assets/js/bootyper.js"></script>

<script type="text/javascript">
% if post.meta('code') in ["false","False"]:
    code_show = false;
% else:
    code_show = true;
% endif
$( document ).ready(code_toggle);
</%block>

Now if I set the code metadata to "false" or "False" then the code cells will be hidden on page load. Otherwise they'll be shown!

That's it! Hopefully this is a good rundown of the basics of optimizing a Nikola theme for Jupyter notebooks and you'll be able to further customize from here. I keep my version of the bootyper theme on github. It contains the features show here plus a few extra features that aren't in the default theme. Some things I'd like to add in the future is having the code cells show some marker when they're hidden so the reader know's they're there, and also to have the toggle button float as the reader scrolls down the page.

In [2]:
a = "Here's the output of a code cell. Use the toggle button to see the code!"
a
Out[2]:
"Here's the output of a code cell. Use the toggle button to see the code!"

Comments

Comments powered by Disqus