nikolaでipynbファイルの公開設定を行う

Nikolaでipynbファイルを使用する方法

Nikolaでipynbファイルを使用する方法ですが、過去にdrillerさんがQiitaに書かれた記事を参考に追加することが可能です。

しかし、このままだとMathJaxが有効化されない、プロットした画像が描画されないなどの問題があったため、下記に簡単に対策を記載しておきます。

事前準備

conf.pyPOSTSPAGESipynb用のエントリを追加しておきます。

ipynbファイルでのブログ記事新規作成

mdファイルでの作成と同じようにnikola new_post -f 形式で形式にipynbを指定すれば良いです。

$ nikola new_post -f ipynb

記事タイトルを聞かれるので入力するとpostsディレクトリ下にipynbファイルが生成されます。

既存ipynbファイルの使用

  • postsディレクトリ下に既存ipynbファイルを配置
  • Jupyter NotebookでMenu -> Edit -> Edit Notebook Metadataを選択
  • 下記のようなエントリを追加(最初の{...}は元々metadataに書かれていたエントリを省略したものです)
{ {...},
  "nikola": {
    "tags": "python,pydata,numpy,scipy,librosa,scikit-learn",
    "title": "2018/2/28にPydata Osakaで'SciPyの概要と各モジュールの紹介'というタイトルで発表しました",
    "date": "2018-06-16 14:3:00 UTC+09:00",
    "type": "text",
    "slug": "4",
    "category": "",
    "link": "",
    "description": ""
  }
}

ブログポストの構築

markdownのときと同じようにnikola buildでブログを構築します。 nikola serveでローカルでの確認を取ります。

$ nikola build
$ nikola serve -b

MathJaxの描画

上記までで記事の追加は可能なのですが、このままだとMathJaxによる数式が描画されません。 描画するためには下記の対処を行います。

  • CSPのdefault-srcに"https://cdnjs.cloudflare.com"を追加
    • 過去記事でも触れたようにconf.pyEXTRA_HEAD_DATAを編集
  • conf.pyで無効化されているMATHJAX_CONFIGのオプションをコメントアウトして有効化

前者によってmathjaxがCDNから読み込まれるようになり、後者によって\$で囲われたインライン数式が描画されるようになります。(MathJaxはデフォルトでは\$によるインライン描画は有効化されていない)

画像、音声の読み込み

上記対策を行ってもipynbファイルに埋め込まれた画像、音声が描画されませんでした。これもCSPによってブロックされていたためです。対処としては、stackoverflowのこのQAを参考に、CSPのdefault-srcにdata:を追加(conf.pyEXTRA_HEAD_DATAを編集)することでipynbにbase64で埋め込まれたdata:imageやdata:audioが読み込まれるようになりました。

参考

nikolaでCSP、Google Analytics、SNSボタンの設定を行う

概要

nikolaでCSP(Content Security Policy)の設定を行った上でGoogle Analyticsの設定を行い、またTwitter、Facebook、はてなブックマークボタンを設置したので下記に作業メモを書いておく。

Content Security Policy(CSP)の設定

CSPについての細かい説明はここでは書かないが、要はXSSを防ぐためにスクリプトや画像などのリソースをどのドメインからなら許容するかの設定である。 トラッキングコードやSNSボタンを設置する場合はこの設定を行う必要がある。設定していない場合はスクリプトや画像の読み込みが許可されないため、ボタンが表示されなかったりJavaScriptが動作しなかったりする。

設定方法

CSPの設定はHTTPヘッダで設定するかMETAタグを用いて設定することができる。 nikolaはstaticファイルを吐き出すgeneratorなので後者のMETAタグを用いた設定を行うこととなる。 METAタグによる設定はHEADタグの最初に書くべきであるが、nikolaではHEADタグの頭に書くようなオプションはないため、仕方なくEXTRA_HEAD_DATAオプションにこのための設定を記述する。 実際に設定しているCSPの設定は下記の通りである。

EXTRA_HEAD_DATA = """
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' http://*.google-analytics.com https://*.google-analytics.com https://code.jquery.com http://*.disqus.com https://disqus.com https://*.disqus.com https://*.disquscdn.com https://*.cloudinary.com http://www.gravatar.com https://www.googletagmanager.com https://*.twitter.com http://*.facebook.com https://*.facebook.com https://*.facebook.net http://*.hatena.ne.jp https://*.st-hatena.com;">
"""

基本的には"default-src"に対して'self'、'unsafe-inline'、および各ドメインに対する設定をhttpとhttpsに分けて記述している形になる。 'self'と'unsafe-inline'についてはGoogleのCSPに関するページに記載があるが、 'self'は現在のオリジンを、'unsafe-inline'はインラインJavaScriptおよびCSSを許可するための記述である。 シングルクォーテーションで囲う必要があるため注意が必要である。 各種ドメインに関する設定については、下記にあるSNSボタン設置などの設定を行いつつ、開発者コンソールを眺めながらブロックされているドメインを一つ一つ許可する、といったような形で行った。

disqus対応

開発者コンソールで見るとコメントシステムとして使用しているdisqusに関してもローディングに関するエラーを吐いていたため、このリンク先を見た上でdisqusに関する記述も追加した。 リンク先ページにも記述があるように、evalを要求してくるような部分は上記CSP設定でもブロックされるが、これはdisqusによる広告の挿入のためであるとのことであるため、特に許可していない。

Google Analyticsの設定

Google Analyticsの設定はCSPと同様にHEADタグ内に書く必要があるため、EXTRA_HEAD_DATAのオプションに追加で設定する。

EXTRA_HEAD_DATA = """
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-48887105-1"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'UA-XXXX');
</script>
<!-- End Google Analytics -->
"""

ここでは従来使われていたトラッキング用のコードではなく、Google Tag Managerを使用するコードの記載としている。"UA-XXXX"のXXXXの部分は各自独自の設定に書き換える必要がある。

SNSボタンの設置

このページSOCIAL_BUTTONS_CODEの設定にAddthisというSNSボタンをまとめて設置するためのサービスを使って設定する旨がデフォルトとして書かれているが、

  • SOCIAL_BUTTONS_CODEはbody末尾に追加されるためレイアウト的に微妙な位置にボタン類が追加される
  • SOCIAL_BUTTONS_CODEのオプションはこのIssueで"Unneeded features"に分類されており、同じ用途であればBODY_ENDのオプションが使える
  • SNSボタンだけでなくaddthisに関するCSP設定も必要となる

などの理由から、結果として各サービスのSNSのボタンを個別に追加した方がトラブルを避けられたため、ここでは個別にどのように追加したかについて記載する。

SNSボタンの設定方法

前述のようにSOCIAL_BUTTONS_CODEのオプションは使わない。代わりにCONTENT_FOOTERのオプションを使用する。CONTENT_FOOTERは当初下記のような記述になっている。

CONTENT_FOOTER = "Contents &copy; {date}         <a href="mailto:{email}">{author}</a> - Powered by         <a href="https://getnikola.com" rel="nofollow">Nikola</a>         {license}"

上記は設定年、著者名、Nikolaによる生成であることをfooterとして設定するための記述となっている。 {date}{email}{author}など、埋め込み文字列のようになっている部分に気付くかもしれないが、 CONTENT_FOOTERオプションはconf.py上では文字列テンプレートとなっており、format関数を用いてformattingされる。 format関数の引数としては、別途設定されているオプションCONTENT_FOOTER_FORMATSが用いられる。 そのため、CONTENT_FOOTER中で{}を用いる場合は、pythonのformat関数の仕様に合わせて{{}}のように記述する必要があることに注意されたい。

SNSボタン設定に関する記述は、CONTENT_FOOTERの後続する部分にタグを埋め込むことで行う。 最終的には下記のような記述となっている。facebookのscript部分の中括弧を'{{'と'}}'に変えている。

CONTENT_FOOTER = """Contents &copy; {date}         <a href="mailto:{email}">{author}</a> - Powered by         <a href="https://getnikola.com" rel="nofollow">Nikola</a>         {license}
<!-- twitter -->
<a href="https://twitter.com/share?ref_src=twsrc%5Etfw" class="twitter-share-button" data-show-count="false">Tweet</a>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<!-- facebook -->
<div id="fb-root"></div>
<script>(function(d, s, id) {{
  var js, fjs = d.getElementsByTagName(s)[0];
  if (d.getElementById(id)) return;
  js = d.createElement(s); js.id = id;
  js.src = 'https://connect.facebook.net/ja_JP/sdk.js#xfbml=1&version=v2.11';
  fjs.parentNode.insertBefore(js, fjs);
}}(document, 'script', 'facebook-jssdk'));</script>
<div class="fb-like" data-action="like" data-size="small" data-show-faces="true" data-share="true"></div>

<!-- hatena -->
<a href="http://b.hatena.ne.jp/entry/" class="hatena-bookmark-button" data-hatena-bookmark-layout="basic-counter" title="このエントリーをはてなブックマークに追加">
<img src="https://b.st-hatena.com/images/entry-button/button-only@2x.png" alt="このエントリーをはてなブックマークに追加" width="20" height="20" style="border: none;" />
</a>
<script type="text/javascript" src="https://b.st-hatena.com/js/bookmark_button.js" charset="utf-8" async="async"></script>
"""

各SNSボタン用コードの取得に関しては以下に記述する。実際の見た目については一番下までスクロールすれば見えると思われる。

twitter

publish.twitter.comでTwitter Buttonsを選び、続いてshare buttonを選ぶ。するとコードが表示されるので、これをCONTENT_FOOTERオプションに追加すれば良い。

facebook

いいね!ボタンの設定方法に関するページのジェネレータで吐き出されたコードを使用する。 「いいね!」するURLは空白、レイアウトは"button"、アクションタイプは"like"、ボタンサイズは"small"、友達の顔は表示する、シェアボタンを追加、のチェックは共にOnにした状態でコードを取得し、そのコードをCONTENT_FOOTERオプションに追加した。

Open Graph対応

facebookのいいね・シェアボタンやGoogle Plusの+1ボタンなどが対応しているOpen Graphへの対応はconf.pyのオプションを有効化することによって実現できる。下記オプションをTrueに設定すると、

USE_OPEN_GRAPH = True

出力されるHTMLのheadタグ内に下記記述が追加される。

<meta property="og:site_name" content="hiromasa.info">
<meta property="og:title" content="nikolaでCSP、Google Analytics、SNSボタンの設定を行う">
<meta property="og:url" content="http://www.hiromasa.info/posts/2/">
<meta property="og:description" content="...">

description内のcontentの記述は省略したが、本文(主にh2など)から抽出した内容が記載される。 なお、USE_OPEN_GRAPHのオプションはこのIssueで"Unnecessary customization"扱いとなっており、将来的にデフォルトでTrueになっている(オプションから消失している)可能性がある。

facebookボタン位置の調整

ジェネレータが吐いたコードをそのまま使うと、facebookボタンが改行されてしまう。 これは単にblock要素であるdivを使っているためであるため、"fb-like"クラスと"fb-root"のidをinlineにするためのCSS設定を行うことでボタンが横並びになるように設定を行う。 また、横並びの配置にした場合においても、facebookのボタンの部分だけ下にずれてしまう。 この理由はボタンウィジェットに対するCSSのvertical-alignがbottomになっているせいである(参考)。 これを調整するためにはvertical-alignをbaselineにしてやれば良い。 これらのCSS設定はHEADタグ内で設定する必要があるため、EXTRA_HEAD_DATAオプションにstyleタグおよび設定を追加する。

EXTRA_HEAD_DATA = """
<!-- for facebook button -->
<style>
.fb-like {
    display: inline;
}
#fb-root {
    display: inline;
}
.fb_iframe_widget > span {
    vertical-align: baseline !important; 
}
</style>
"""

はてなブックマークボタン

設置方法に従って出力されたコードをそのまま使用している。 ボタンのラベルは非表示、ブックマーク数は表示、保存するURLはページのURLを使う、に設定して取得したコードをCONTENT_FOOTERに追加している。

最終的なEXTRA_HEAD_DATA

上記の複数箇所で触れたEXTRA_HEAD_DATAは最終的に以下のような形で記述している。

EXTRA_HEAD_DATA = """
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' http://*.google-analytics.com https://*.google-analytics.com https://code.jquery.com http://*.disqus.com https://disqus.com https://*.disqus.com https://*.disquscdn.com https://*.cloudinary.com http://www.gravatar.com https://www.googletagmanager.com https://*.twitter.com http://*.facebook.com https://*.facebook.com https://*.facebook.net http://*.hatena.ne.jp https://*.st-hatena.com;">

<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-48887105-1"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'UA-XXXX');
</script>
<!-- End Google Analytics -->

<!-- for facebook button -->
<style>
.fb-like {
    display: inline;
}
#fb-root {
    display: inline;
}
.fb_iframe_widget > span {
    vertical-align: baseline !important; 
}
</style>
"""

参考

Nikolaでブログ構築してGithub Pagesに設置

Nikolaとは

Python製のstatic site generator。python製だとpelicanの方が有名だがipynbが使えると聞いたのでこちらを使うことにした。

Nikolaでブログを生成

インストール

pipでインストールできる。

$ pip install nikola
$ pip install webassets

後述するbuildの際などに下記のようなメッセージが出るので合わせてwebassetsもインストールしておくと良い。

[2018-01-08T01:42:01Z] WARNING: Nikola: In order to USE_BUNDLES, you must install the "webassets" Python package.
[2018-01-08T01:42:01Z] WARNING: bundles: Setting USE_BUNDLES to False.

ブログの雛形生成

nikolaはgitのようにnikola サブコマンドの形式で様々なコマンドを実行できる。 雛形の生成のためにはnikola initを実行する。

nikola init blog_name

これを実行するとCUIベースで対話的に様々な設定が可能。 commentシステムとして何を使うかを聞かれるが、後でconf.pyを書き換えれば設定が可能な模様であるため一旦は空白で良い。

なおGetting Startedではinitの際に引数に--demoを付けているが、 この引数を付けるとデモ用コンテンツが生成されてしまうので普通に真っさらな状態で始めたいならば付けない方が良い。

新記事生成

新記事生成は下記の通り。

$ nikola new_post -f markdown  # formatを-fで指定(デフォルトはReStructuredText形式)

実行すると記事タイトルを聞かれるので打ち込むと-fで指定した形式のファイルがposts以下に生成される。 また、引数に-eを付けるとそのままエディタでの編集画面となる。

生成されたファイルの冒頭には下記のような形のヘッダーが付いている。

<!--
.. title: Nikolaでブログ構築してGithub Pagesに設置
.. slug: 1
.. date: 2018-01-07 23:35:28 UTC+09:00
.. tags: nikola, python, github
.. category: 
.. link: 
.. description: 
.. type: text
-->

titleは記事タイトルである。 slugはurlの一部となる文字列である。記事タイトルをアルファベットに直したものが記載されているが、適当に修正すると良い。1にしておくとhttp://www.hiromasa.info/1/のようなURLとなる。

固定ページ生成

固定ページはpostではなくpageとして生成する。

$ nikola new_page -f markdown pages/about

生成した固定ページを上部ナビゲーションに配置する方法は下記のようにconf.pyNAVIGATION_LINKSに書けば良い。

NAVIGATION_LINKS = {
    DEFAULT_LANG: (
        ("/archive.html", "文書一覧"),
        ("/categories/", "タグ"),
        ("/pages/about/index.html", "About"),
        ("/rss.xml", "RSSフィード"),
    ),
}

ブログ構築

postやpageを上記のように生成しただけでは単なるテキストが生成されているだけなので、これらを元にブログを構築する。

$ nikola build

これでブログが構築される。 ローカルで確認するためには下記を実行する。

$ nikola serve -b  # --browserでも良い

ブログテーマ設定

デフォルトはbootstrap3になるようであるが、他のものに変えたい場合はthemesのページから好きなものを選ぶ。 ここではlibrettoのテーマを選んだ。 wordpressの同名テーマをベースにしたものらしい。

テーマのインストールは下記の通り。

$ nikola theme -i libretto

実行するとthemesディレクトリ以下にlibrettoのテーマがダウンロードされる。

このテーマを使用するためには更にconf.pyを編集する必要がある。

# Name of the theme to use.
# THEME = "bootstrap3"
THEME = "libretto"

この上でnikola buildを実行すれば更新後のテーマに変わっている。 記事の一文字目だけ大きく表示されるのが若干気に入らないがとりあえずこれで行く。

※2017/1/8追記、librettoだとsyntax highlightが効かないようなのでbootstrap4のテーマに変えた

Github Pagesに配置

HandbookのDeploymentの項を見るとGithubでホスティングする方法が記載されている。ここではGitHubのuser page(wrist.github.io)に配置することを考える。

1. gitリポジトリの設定を行う

過去にwrist.github.ioリポジトリを作成しているため、ブログのルートでgit initしてからremoteを設定する。

$ git init
$ git remote add origin git@github.com:wrist/wrist.github.io

2. (必要であれば)conf.pyを編集する

必要であれば下記を編集する。今回は不要であるため特に変更していない。

# siteのソースがdeployされるブランチ名、srcが推奨とのこと
GITHUB_SOURCE_BRANCH = 'src'
# HTMLファイルがdeployされるブランチ、user pageの場合はmaster
GITHUB_DEPLOY_BRANCH = 'master'
# gitのremote名
GITHUB_REMOTE_NAME = 'origin'
# ソースブランチを自動的にコミットしプッシュするかの設定
GITHUB_COMMIT_SOURCE = True

3. .gitignoreを追加

下記内容を.gitignoreに追加する。

cache
.doit.db
__pycache__
output

4. deploy用のコマンドを実行

下記コマンドを実行することでdeployが可能。

$ nikola github_deploy

下記のように表示された場合はpip install ghp-import2でghp-import2をインストールする。

[2018-01-08T04:16:04Z] ERROR: Nikola: In order to deploy the site to GitHub Pages, you must install the "ghp-import2" Python package.
[2018-01-08T04:16:04Z] ERROR: Nikola: Exiting due to missing dependencies.

通常はこれで改めてnikola github_deployを実行すれば終わりのはずである。 しかし、今回は過去に作成済の既にmasterが存在しているリポジトリにpushしようとしているため、当然先にfetchしろと警告が出て失敗する。

[2018-01-08T04:17:10Z] INFO: github_deploy: ==> ['ghp-import', '-n', '-m', 'Nikola auto commit.\n\nSource commit: a37690e35a31dc4a108f53c62b8ee17e783aa28f\nNikola version: 7.8.11', '-p', '-r', 'origin', '-b', 'master', 'output']
To github.com:wrist/wrist.github.io
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'git@github.com:wrist/wrist.github.io'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

仕方ないのでこの記事に倣い、先にmasterを強制pushする。

$ git push -f origin master
$ nikola github_deploy

これにて無事にhttp://wrist.github.ioで閲覧できるようになった。

5. 独自ドメインでの使用

github pagesを独自ドメインで使用する場合にはCNAMEファイルをリポジトリのルートに配置しておく必要があるが、このためにnikolaではソースブランチのfilesの下にCNAMEファイルを配置しておけば、これをbuild時にoutputディレクトリにコピーしてくれる。

コメントシステムの設置

init時には空白にしておいたコメントシステムを有効化する。 ここではdisqusを用いることにする。conf.pyに下記を追記する。

COMMENT_SYSTEM = "disqus"
COMMENT_SYSTEM_ID = "hiromasa-info"

COMMENT_SYSTEM_IDはdisqusの場合はshortnameを設定する。 shortnameはdisqusのsiteのsettingsから確認できる。

その他

記事のデフォルト形式をmarkdownに変えたいが方法が分からないためHandbookを読んでもう少し調べる必要があるが、 ReStructuredTextで書くとExtensionとしていくつかのdirectiveやroleが使えるようでもあるため、もう少し使ってみてから考えたい。

参考