BlogをHatena blogからGithub pagesに移行しました。
Markdownで記事を書いて、Rust製のstatic site generator zolaでhtmlを生成する構成です。
本記事では、移行にあたって調べた事や行った設定について書きます。
Zolaのversionは0.16.1
です。
機能の概要
最初にzolaでできることの概要をまとめました。
- Markdownをtemplateでhtlmに変換する仕組み
- Markdown中から呼び出せるDSL(shortcodes)
- Markdownをparseしてtemplate rendering時に参照できる変数として提供
- Template engine(tera)の拡張
- Draftsによる公開の制御機能(build対象のfilterling)
- Syntax highlight(codeblockからhtml+cssへの変換)
- Taxonomiesによる記事のtagging
- 記事(page)および記事グループ(section)のmetadataの拡張(extra)
- Sitemap生成
- Feed生成
- Localの確認環境
- Redirect設定
- Pathのslugify
- 外部Link,内部Linkの有効性確認
- Sassサポート(ただし
LibSass 3.6.4
) - Github actions deploy用action
- Theme
また、本記事では言及できていないのですが、SearchやMultilingual対応機能もあります。
移行のきっかっけ
Hatena blogには特に不満はなかったです。
ただ、記事をgitで管理して、完成したらHatena blogのUIに貼り付けていました。これがpushだけで完結したら楽だなーと思っていました。
そんな時にzolaを知り、試してみたらsimpleで使いやすかったので移行してみることにしました。
Zolaの使い方
Installについては公式のInstallationを参照してください。
自分はsourceからbuildしました。
Zolaには以下のコマンドがあります。
- Project初期化用の
zola init
- 書いている記事を手元で確認するための
zola serve
- Build及び記事のlinkが生きていることを確認する
zola check
- 最終的に公開するhtml等を生成する
zola build
Zolaのdirectory構造
まずはzola init
から始めます。
> What )
> Do
> Do
> Do
zola init
実行後にいくつか質問に答えるとdirectoryが作成されます。
なお、messageにある通り設定は後から変えられるので適当に答えても特に問題ありません。
directory構成を確認します。
config.toml
がzolaの設定fileですcontent
がmarkdownを格納するdirectoryですsass
は適用するcss(sass)を配置しますstatic
に公開される画像等のasset fileを置きますtemplates
にmakrdown fileをhtmlに変換する方法を指示するtemplateを置きますthems
適用するthemeの格納場所です
zola
コマンドを実行するとcontent配下のmarkdown filesがtemplatesに従ってhtmlに変換され、sass配下のcssとstatic配下のasset fileと共にpublic
(設定で変更可) directoryに出力されます。
Sectionとpage
さっそくmarkdownを書いていきたいところですが、その前にzolaのSectionとPageについて説明させてください。
まず、Pageはcontentとして公開するmarkdownのことです。Pageには以下のようにmetadataを付与することでtemplateで処理する際に参照することができます。
+++
title = "🚛 BlogをZola + Github Pagesに移行した"
slug = "migrated-blog-to-zola-and-github-pages"
date = "2023-02-13"
draft = true
description = "Rust製 static site generator zolaとGithub Pagesでblogを公開するまで"
[taxonomies]
tags = ["etc"]
+++
Blogを...
上記は本記事のmetadataです。
title
記事のtitleslug
記事のpathに利用されるdate
公開日、pageを日付でsortする場合に参照されるdraft
draftの設定、後述しますdescription
description templateで必要なら参照できるtaxonomies
いわゆるtagでzolaが提供するpageの分類機能、こちらも後述
その他aliases
でredirect用のpathを設定できたりもします。詳しくは公式docを参照してください。
次にSectionを作成します。content
配下にdirectoryを作成し、_index.md
fileを配置するとそのdirectoryがzolaからSectionとして認識されます。
Sectionは作らなくても良いのですが、同じ分類のpageをSectionにまとめておくとtemplateでlistとして参照できて便利です。
今回、blogの記事はentry
Sectionとして作成することにしました。
まずは、content/entry/_index.md
を作成します。content
配下はそのまま公開時のpath名になるので、記事のURLはhttps://blog.ymgyt.io/entry/{page_metadata.slug}
になります。
Page同様にSectionにも_index.md
にmetadataを記述できます。
+++
title = "Blog entries"
sort_by = "date"
template = "entry.html"
page_template = "entry/page.html"
insert_anchor_links = "heading"
+++
title
templateから参照できますsort_by
pageのsort方法、templateでsortされている前提で扱えますtemplate
Section pageのtemplateの指定page_template
defaultで利用するpage共通のtemplateの指定insert_anchor_links
markdownの見出し(## Chapter2
)にanchor(#
)用のlinkを作成するかの指定
Page同様、詳しくは公式docを参照してください。
Sectionで/entry
や/entry/hello-world
のようなpathでアクセスがあった際にrenderingに利用するtemplateが指定できたので、次はtemplateについて見ていきます。
Templateの書き方
ZolaではTeraというtemplate engineが利用されています。
Goのtemplate等、なにかしらのtemplate engineを利用したことがあればすぐに使えると思います。 使い方は公式のdocがあるので参照してください。
以下ではとりあえずこれだけ知ってれば書き始められるくらいのことを書きます。
{{ expression }}
{{
と}}
で囲むとexpressionを書けます。{{ config.base_url }}
のように変数にアクセスできます{{ page.date | date(format="%m/%d") }}
のように|
でbuilt-in関数にpipeできます
{% %}
がstatementでif, loop, include, extend等の制御が書けます{# #}
がcommentでhtmlに出力されないcommentが書けます
上記を前提にしてまず、どのtemplateがどのpath用のhtmlを生成するためのものかはzolaの規約で決まっています。(ないしは指定できます)
その際、zolaがtop levelで参照できる変数を用意してくれるのでtemplateではそれを前提にします。
例えば、/entry/hello-world
用のhtmlを生成する為に際はzolaは/entry/hello-world/index.md
(もしくは/entry/hello-world.md
)を参照することを知っているので、当該pageのmetadataを保持した変数をtemplateに渡してくれます。
{{ page.title }}
{{ page.date }}
{{ page.content | safe }}
の様なことが書けるわけです。
以降はzolaというよりはteraの話になってしまいますが、templateに関して調べたことを書いていきます。
まず、templateの共通処理に関してはinclude
とextend
があります。
include
は被include側のcontentがinclude側にそのまま展開されます。
extend
は被extend側で定義したblockをextend側のcontentで置き換えることができます。
自分は各pageからextendするbase用のtemplateを一つ用意して使いました。child extend (parent extend root)のようにextendしていくこともできます。
{% include "base/head.html" %}
{# head.html側に定義するとextend側でoverrideできない #}
{% block title -%}
{{ config.title }}
{%- endblock title %}
{% block description %}
{% endblock description %}
{% include "base/header.html" %}
{% block content %}
{% endblock content %}
{% include "base/footer.html" %}
{% block xxx %}
から{% endblock xxx %}
までがextend側にcontentを提供してもらう想定の場所です。
extendされなかった場合用にdefaultのcontentを書いておくこともできます。
被include templateにblockが定義してあってもextendできなかったのがはまりポイントでした。
実際にentry pageで利用するtemplateから以下のようにしてextendしました。
{% import "macro/title.html" as macro %}
{% extends "base/base.html" %}
{% block title %}
{{ macro::title(title=page.title) }}
{% endblock title %}
{% block description %}
{% if page.description %}
{% endif %}
{% endblock description %}
{% block content %}
{{ page.content | safe }}
{% endblock content %}
{% import "macro/title.html" as macro %}
としているところはteraのmacroの処理です。
<head>
の<title>
を"記事のtitle | Blog name"のようにしたかったので以下のようなmacroを用意しました。
{% macro title(title) %}
{{ title }} | {{ config.title }}
{% endmacro title %}
teraではfor,if,variableへのassignができるので、やろうと思えばなんでもできそうです。
例えば、記事のTableOfContent(TOC)を作る場合、専用の機能があるのではなくtemplateで作ることができます。
{% for h1 in page.toc %}
{{ h1.title }}
{% if h1.children %}
{% for h2 in h1.children %}
{{ h2.title }}
{% endfor %}
{% endif %}
{% endfor %}
page
にはArray<Header>
型のtoc
fieldがあるのでそれをiterateしています。
どんな変数が参照できるかは公式docを参照してください。
記事の一覧を表示する/entry
も以下の様にして作成しました。
{% block content %}
Entries
{% for year, pages in section.pages | group_by(attribute="year") %}
{{ year }}
{% for page in pages %}
{{ page.title }}
{{ page.date | date(format="%m/%d") }}
{% endfor %}
{% endfor %}
{% endblock content %}
Shortcodes
基本的にmarkdown fileが先頭のmetadataの記述を除いてはzolaに処理されることを意識しなくて良いようになっています。
ただし、markdown側から生成したいhtmlを指定したい場合もあるかと思います。その際にmarkdown側から呼び出せるDSLがshortcodesとして提供されています。
自分はHatena blogから移行するにあたって、記事への画像の埋め込みにHatena blog側の機能を利用していたので、画像まわりの処理に利用しました。
具体的には、記事中に画像を貼りたいところで
{{
figure(caption="Component diagram", images=["images/cqrs_component_diagram.png"] )
}}
上記のような処理を書いて、<figure>
tagを生成しました。
このfigure()
呼び出しを有効にするにはtemplates/shortcodes/figure.html
を作成します。
file名が関数(shortcodes)名になります。
templateの中では、引数(caption
, images
)が変数として参照できるので下記のように参照できます。
{% if href %}
{% endif %}
{% for src in images %}
{% endfor %}
{% if caption %} {{ caption }} {% endif %}
{% if href %}
{% endif %}
(img.alt
が設定できていないので改善したい)
Tag管理
よくある記事にtagをつける機能はTaxonomiesとしてサポートされています。
今回はtags
というtaxonomyを定義して、記事ごとにtags = ["rust", "etc"]
のようにtag付けしていきます。
まず、config.toml
に以下のように定義します。
= [
{ = "tags", = true, = true },
]
これでzolaにtags
というtaxonomyがあると宣言できます。
記事にtagを振るには記事のmetadata(/content/entry/hello-world/index.md
)にtags
を定義します
+++
title = "🚛 BlogをZola + Github Pagesに移行した"
// ...
[taxonomies]
tags = ["etc"]
+++
tags
というtaxonomiesが定義されると、zolaは/tags
と/tags/etc
のようなtag一覧とtagごとの記事一覧のpageを用意しようとします。
そして、templates/tags/list.html
とtemplates/tags/single.html
がそれぞれ対応するtemplateなのでこれを用意しておきます。
Taxonomies用のtemplateがない場合それぞれtemplates/{taxonomy_single.html,taxonomy_list.html}
が参照されます。
list.html
,single.html
ではそれぞれtaxonomyの情報を変数で渡してくれるのでtemplateではそれを利用します。
Syntax highlight
Markdownからhtmlに変換する際にどうしても必要になるのはsyntax highlightではないでしょうか。
手元でcodeblockを書いている分にはplugin等でsyntax highlightが効いた状態で見えるかと思いますが、html化するにあたっては、markupした上でcss用のclass付与等が必要になると思います。
Zolaでsyntax highlightを有効にするにはconfig.toml
で以下のように設定します。
[]
= true
= "monokai"
指定できるthemeについては公式docを参照してください。
markupとcssのclass付与だけ行い、cssは自分で管理したいというユースケースにも対応しています。
その場合、highlight_theme = "css"
を指定します。(css
は特別扱いされます)
さらに
[]
= [
{ = "base16-ocean-dark", = "syntax-theme-dark.css" },
{ = "base16-ocean-light", = "syntax-theme-light.css" },
]
を指定すると適用されるcssを出力してくれます。
自分はhighlight_theme = "css"
を指定して、nordのthemeをcolor
を調整して利用しました。
Sass
cssに関してはLibSassが利用されています。
sass/style.scss
を書いておくと、public/style.css
が出力されます。
@use
は使えず、@import
を利用しました。
Theme
Themeの適用に関しては最初わかりづらかったです。
結論からいうと自分はthemeは利用せずcssを書きました。
themeを利用するには、themes
directory配下に利用したthemeのrepositoryをgit cloneやsubmodule等でfetchします。
その後、config.toml
でtheme = "my_theme"
で指定します。
これで何が起きるか最初はわからなかったのですが、現状の理解は以下です。
Zolaのfile search pathにthemes
配下が含まれている。
そのため、sectionのtemplateを検索する際に、userが専用のtemplateを指定しないとsection.html
にfallbackされる。さらにそのfileがtemplates
以下に定義されていないとthemes
配下が検索される。
利用するtheme側のrepositoryにtemplates/section.html
があるとそれが利用される。
結果的にthemeが適用される。
なので、自分はthemeを適用する前に、page, entry section用のtemplateを既に作成して指定していたのでthemeを設定しても一向に反映されませんでした。
また、自分はtaxonomiesにtags
を定義しましたが、theme側ではuserがtags
を定義するかはわからないので、taxonomiesのfallback用のtaxonomy_list.html
を用意するまでしかできません。
Github actions
Github Pages公開にはGithub actionsを利用しました。
公式のexampleをほぼそのまま利用しました。
# On every push this script is executed
on:
push:
branches:
- main
name: Build and deploy GH Pages
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3.0.0
- name: build_and_deploy
uses: shalzz/zola-deploy-action@v0.16.1-1
env:
# Target branch
PAGES_BRANCH: gh-pages
TOKEN: ${{ secrets.GITHUB_TOKEN }}
main branchにpushするとgh-pages
branchのtop levelにpublic
以下が展開されます。
gh-pages
branchをGithub pages側のbranchに設定します。
Custom domainの設定
Hatena blogでは自分のdomain(blog.ymgyt.io
)を利用していたのでGithub pagesでもcustom domainの機能を利用することにしました。
実際にやってみたことろ以下の作業が必要でした。なお、DNS管理はAWS Route53を利用しています。
- zola
config.toml
の設定 static/CNAME
fileの作成- Github pages custom domainの設定
- Route53 CNAME Recordの作成
以下それぞれの作業を具体的に見ていきます。
設定に利用したdomainはblog.ymgyt.io
, github account名はymgyt
という前提です。
Zola config.toml
の設定
zola設定fileのconfig.toml
に利用するurlを設定します。
= "https://blog.ymgyt.io"
static/CNAME
fileの作成
Github pagesではcustom domainを利用する場合、top levelのdirectoryにCNAME
というfileがあることを期待しています。 意外とこのCNAME
に何を書けばいいのか説明がなかったのですが、Troubleshoot a custom domainに説明がありました。
- The CNAME file can contain only one domain. To point multiple domains to your site, you must set up a redirect through your DNS provider.
- The CNAME file must contain the domain name only. For example, www.example.com, blog.example.com, or example.com.
ということで、domainをCNAME
に記載します。
static/
配下に置いておけばbuild時にtop levelで出力されるのでGithub pagesの期待通りになります。
Github pages custom domainの設定
CNAME
fileをpushした段階でGithub側でcustom domainの設定が有効になり、UI自体では特になにも設定しませんでした。
一応documentにはRepository Settings > Pages > Custom domainにdomainを入力する必要があると書かれていました。
Enforce HTTPSは有効にしました。
Route53 CNAME Recordの作成
Route53で以下のrecordを作成しました。(実際はhatena用のCNAME recordを更新しました)
- Record name:
blgo.ymgyt.io
- Record type:
CNAME
- Value:
ymgyt.github.io.
; <<>> DiG
;;
;
これでcustom domainの設定は完了です。
Linkの確認
zola check
でbuild及びLink(anchor tagのhref
)の確認ができます。
codeblockでsyntax highlighが効いていない場合もwarningを出してくれます。(watがなかった)
Linkの確認ではmod.rs#L10-L20
のようなfragmentが有効かどうかまでチェックしてくれます。
ただし、githubでの行間のhighlightを#L10-L20
のようにしていると無効と判定されてしまいました。
このような場合は以下の様にconfig.toml
でskipすることができます。
[]
= [
"http://[2001:db8::]/",
]
# Skip anchor checking for external URLs that start with these prefixes
= [
"https://caniuse.com/",
]
数年前の記事ですとfragmentが無効になっているケースも多々あったのでfragment付ければいいものでもないなと思いました。
また、zola check
をCIで回そうかとも考えたのですが、linkは外部の要因で壊れたりもするので、最初は見送りました。
Draft
記事を作成中はまだ公開したくないという状態があるかと思います。
その際は、pageのmetadataでdraft = true
を指定するとdraft扱いとなります。
Draftのpageはzola build
時に無視されるので、本番に公開されなくなります。
Localでdraftを確認するには、zola serve --drafts
のように--drafts
flagを付与します。(buildも同様)
その他ecosystem
Feeds(RSS)
config.toml
にて
= true
= "atom.xml"
を指定すると生成してくれます。
Sitemap
defaultでpublic/sitemap.xml
を出力してくれます。
Google search consoleで指定したら問題なく認識されました。
404
templates/404.html
を書いておくと404 pageを作成できます。
robots.txt
defaultで以下のfileが作成されます。templateで上書きもできます。
User-agent: *
Disallow:
Allow: /
Sitemap: https://blog.ymgyt.io/sitemap.xml
zola serve
の注意点
zola serve
でちょっとはまった点があったので書いておきます。
serveで立ち上がるhttp serverは末尾の/
がない場合にreedirectして/
を付与するという挙動はしません。
https://github.com/getzola/zola/issues/1781
またこれに関連して、記事から画像等のassetへlinkを付与する際に以下の二つが候補になりました。
static/
配下に置くcontent/entry/hello-world/
等のpageと同一のdirectoryに置く(collocation)
pageと同一のdirectoryに置いた際に、linkは相対pathでimages/aaa.png
のように書くこともできます。
そうすると末尾に/
を付与せずにアクセスすると当該リソースはnot foundになります。
この問題はget_url(path="/images/path/to/aaa.png")
のように何らかの方法で絶対pathにすることで回避できます。
また、Github pagesは末尾/
へのredirectをしてくれる仕様のようでlocalでのみ問題となりました。
個人的には関連するfileは近くに置いておきたいので、page用にdirectoryをきってそこに関連fileを置く様にしています。
おわりに
ここまで駆け足でzolaの概要を見てきました。
正直、全て公式docに書いてある内容ですが、自分の整理もかねて記事にしました。
Zolaはとてもsimpleでありながらここ設定したいと思ったら大抵、設定させてくれるので使ってみたくなりました。 多少のshortcodesというDSLとmetadataを受け入れられればmarkdownをほぼそのままで利用できるので別システムへの移行も特に問題なくできそうな点も良いと思っています。
個人的にはまたひとつ開発toolのrust化が前進したので満足です。
移行とは関係ない話
ここからは移行とは直接関係ない話です。
今回の移行に際して、htmlとcssをtemplate/scssというlayerはあるにしても、ほぼ素でかけてとても楽しかったです。
Frontendの複雑化(typescript, js-runtime, react, build system, module system,...)にともなって、なかなかhtmlやcssを直接扱う機会がなかったのが正直なところでした。
なので、あれhtmlの<head>
になに書けばいいんだっけ?となりました。 そこで、HTML解体新書-仕様から紐解く本格入門とかを読んだりしました。
Faviconに関してはちょうどHow to Favicon in 2023: Six files that fit most needsをTLで教えてもらったので参考にしました。
CSSに関してはもっと大変で、とりあえず本屋にいっておもしろそうな本をいくつか買ってきました。
なかなか今の自分にちょうどいい本がなく、MDNに書いてあることだったり、大規模サイトを複数人でメンテできるようにいかに保守性高めるかの設計論だったりでちょうどいい本が見つかりませんでした。
Every Layoutはとてもおもしろく、CSSはbrowserへの詳細な指示ではなく、ガイドであるというような趣旨のことが書いてあったり、intrinsicなlayoutではmedia-queryは不要みたいな考えに触れ視座が上がったけれど手が動かなかったりしました。
結局、自分に一番合っていたのはCSS FOR JAVASCRIPT DEVELOPERSでした。
$400(5万くらい)払いましたが、かなりよかったです。
おかげで、reset/normalize系のcssから自分で書けて、知らないcss propertyが適用されていないという状況を達成できました。(当たり前かもしれませんが)
いくつかとても参考になった記事も貼っておきます。
- The Surprising Truth About Pixels and Accessibility
- An Interactive Guide to Flexbox
css flexboxの検索結果は汚染されていて、これが最初にでればどれだけ助かったかと思います
- Color Formats in CSS
- colorはhsl派になりました。devtoolでshiftでcolor formatを変換できるのも知りませんでした。