Menu

有些人可能已經注意到這個部落格是用 Pelican 所寫成並且 host 在 Github 上。這篇主要紀錄如何使用 Jinja2 自訂主題。

Pelican 是一個用 Python 寫的靜態網頁生成器, 可以幫我們把 reStructedText, Markdown file 甚至 Jupyer notebook 轉成靜態的 HTML 檔案。 靜態網頁的好處就是我們不需要一個 web server 或者是資料庫來管理內容, 可以把 HTML 檔案 host 在想要的地方,比方說 Github Pages。用 Pelican 官網一句來介紹的話就是:

Pelican is a static site generator, written in Python, that requires no database or server-side logic. - Pelican Blog

Google 一下你會發現除了 Pelican 以外還有很多其他像是 Jekyll, Hexo 等靜態網頁生成器。 之所以會選擇 Pelican 是因為以下幾點:

  • Pelican 是用 Python 寫的,讓 Python 開發者(我)很容易客製化
  • 可以把 jupyter notebook 轉成 HTML,這對每天寫一堆 notebooks 的資料科學家很友善
  • 主題是用強大的 Jinja2 模組引擎建立,可以用前人寫好的主題或是自己寫 templates,自由度很高,也是本篇重點。

如果你的需求類似而且想要自己架一個部落格,可以現在就跳入 Pelican Quickstart,有問題再回來看這篇。

Jinja2 是 Python 知名的模組引擎 (templating engine),可以有系統地產生 HTML,很常出現在 Flask 或是 Django Apps 裡頭。以下介紹在建立 Pelican blog 時常用到的功能。

再利用 HTML 區塊

比方說我們可以建立一個汎用的 template base.html 來定義整個部落格共用的資訊,像是 header 裡頭要 import 的 css / favicon 等等:

<!DOCTYPE html>
<html lang="en">
<head>
{% block head %}
    <link rel="stylesheet" type="text/css" href="css/vendor.css">
    <link rel="icon" href="images/favicon.ico" type="image/x-icon"/>
{% endblock head %}
</head>
<body>
    {% block content %}
        <p>部落格內容</p>
    {% endblock content %}
</body>

注意到上面的 {% block head %} jinja2 語法。會在多個 HTML 檔案重複使用的部分我們可以用 {% block BLOCKNAME %} 以及 {% endblock BLOCKNAME %} 包起來,然後在獨立顯示一篇文章的 article.html 裡頭我們可以定義:

{% extends "base.html" %}
{% block head %}
  {{ super() }}
  <title>文章標題</title>
{% endblock head %}
<body>
    {% block content %}
        <p>文章內容</p>
    {% endblock content %}
</body>

上面的 code 基本上是告訴 jinja2 article.html 要繼承 base.html 的所有內容,而在 head block 除了用{{ super() }} 繼承 base.html 的內容以外,在下面再追加新的內容。而 content block 則是完全取代。

因此最後 article.html 會被渲染成:

<!DOCTYPE html>
<html lang="en">
<head>
    <link rel="stylesheet" type="text/css" href="css/vendor.css">
    <link rel="icon" href="images/favicon.ico" type="image/x-icon"/>
    <title>文章標題</title>
</head>
<body>
    <p>文章內容</p>
</body>

為當前文章取得前/後一篇文章連結

Pagination 範例: 顯示前後文章連結

依照主題不同,有些主題可能文章頁面裡頭並沒有提供前一篇/後一篇文章的連結。要像上圖為每一篇文章取得前後文章的連結,可以在 article.html 裡存取 articles Variable 並使用 jinja2 namespace 來取得前後文章(namespace 要在 jinja 2.10+ 以後才能使用)

{# get prev- and next-article for pagination #}
{% set ns = namespace(found=false, prev=None, next=None) %}
{% for a in articles %}
    {# 要使用 break 要安裝 extension, 最佳化效率可省略 #}
    {%- if ns.found %}{% break %}{% endif %}

    {# 假設文章標題不會重複, unique #}
    {% if a.title == article.title %}
        {% set ns.found = true %}
        {% set ns.prev = loop.previtem %}
        {% set ns.next = loop.nextitem %}
    {% endif %}

{% endfor %}

上面的 code 會 iterate 所有文章,當遇到當前文章的時候利用 loop.previtem 以及 loop.nextitem 把前後文章記下來。 jinja2 預設是無法在 loop 裡頭改變變數的值,但使用 namespace 即可。

接著就能利用剛剛取得的前後 article 物件來渲染前後連結:

{# 方便起見的 assignment %}
{% set prev_article = ns.prev %}
{% set next_article = ns.next %}

{% if prev_article %}
<div>
    <a href="prev_article.url" rel="prev">
        <span>Previous Post</span>
        {{ prev_article.title }}
    </a>
</div>
{% endif %}
{% if next_article %}
<div>
    <a href="next_article.url" rel="next">
        <span>Next Post</span>
        {{ next_article.title }}
    </a>
</div>
{% endif %}

傳參數給子 template

有時候多個 templates 會使用類似的 HTML,像是當首頁 index.html 以及部落格 blog.html 都用相同格式渲染最新幾篇文章時,我們可以定義一個 article_entries.html 如下:

{# 簡化版 #}
{% for article in articles %}
    <article class="col-block">
        <a href="{{ SITEURL }}/{{ article.url }}">{{article.title}}</a>
        <p>{{ article.summary }}</p>
    </article>
{% endfor %}

注意這時候如果直接在 index.html 使用

{% include 'article_entries.html' %}

是會出現錯誤的。理由是被 include 的 article_entries.html 看不到定義在 index.html 的 articles 變數。

解決方法是在 index.html 裡透過 {% with %} 語法定義一個 scope:

{# 選擇前五篇文章來渲染 #}
{% set articles_to_show = articles_page.object_list[5] %}

{# 定義 scope #}
{% with articles=articles_to_show %}
    {% include 'article_entries.html' %}
{% endwith %}

使用 with 的好處是可以把子 template article_entries.html 當作 function 來使用,我們可以依照母 template 的需要,傳進想要渲染的文章即可。

Reference

跟資料科學相關的最新文章直接送到家。
只要加入訂閱名單,當新文章出爐時,
你將能馬上收到通知