Obsidian → Zenn の運用方法
ざっくり
最近, Obsidian でノートをとっています.
これまでは, その一部を Hugo で作成している自分のブログで公開していました.
(Obsidian → Hugo の運用方法)
今回は, それをさらに一歩進めて, Obsidian でとっているノートを簡単に Zenn にエクスポートする 方法を模索してみました.
そして, おそらくこの方法は Hugo から Zenn へのエクスポート にも適用できると思います.
エクスポート前後の状況
Obsidian の Vault (フォルダ) 構成
Obsidian → Hugo の運用方法 にある通りです.
どうしてこの構成を採用しているかは ↑ の記事で説明しています.
.
|-- .obsidian/
|-- _template/
|-- private
|-- notes
|-- ABC.md
|-- DEF.md
|-- static
|-- example.png
|-- example.jpg
|-- public
|-- notes
|-- GHI.md
|-- JKL.md
:
|-- static
|-- sample.png
|-- sample.jpg
Zenn のフォルダ構成
Zenn CLI で記事をアップロードするため, ↓のようなフォルダ構成になります.
.
|-- articles/
|-- images/
|-- package.json
|-- books/
:
エクスポート後はこうしたい
ファイルの移動先
こんな感じ ↓ で Vault の public/
以下をそれぞれ適切なフォルダに出力したいです.
- ノート:
public/notes/*.md
→articles/*.md
- 画像:
public/static/*
→images/*
ちなみに, 私は自分のブログ用のノートも Zenn 用のノートもすべて public/notes/
以下に配置しています.
そのうち Zenn 用の記事には, front matter に zenn: true
の項目を追加しています.
なので, 正確には public/notes/*.md
のうち zenn: true
のものだけをエクスポートしたいです.
ファイルの変換内容
Zenn で適切に処理してもらうには, Hugo とは違った気づかいが必要になります.
トピックの設定
Zenn では記事のカテゴリーを トピック で指定します.
具体的には, 記事の front matter で ↓ のように topics
を設定する必要があります.
topics:
- obsidian
- hugo
- zenn
ただ, 私は, Obsidian や Hugo での カテゴリー分けを topics
ではなく, tags
で管理しています.
なので, tags
→ topics
というようにキーを書き換える必要があります.
内部リンクの処理
Obsidian では Vault 内の他のノートを [[ファイル名 | 表示名]]
の形式で参照できます.
Hugo であれば, これを [表示名](ファイル名.md)
の形式に変換すれば, 有効なリンクとして扱ってくれます. (Obsidian → Hugo の運用方法)
一方, Zenn では内部の記事をパスによって指定することはできないようです. (2022/02/21 現在)
したがって, リンクは [表示名](https://qawatake.com/notes/ファイル名)
のように自分のブログを指すように変換するか, あるいは [表示名](https://zenn.dev/qawatake/articles/ファイル名
のように Zenn のサイトを指すように変換する必要があります.
今回は, 後者の [表示名](https://zenn.dev/qawatake/articles/ファイル名)
の形式に変換してみたいと思います.
画像の埋め込みの処理
Zenn CLI では images
フォルダに画像を配置すれば, 自動的にアップロードして参照することができるようになります. (GitHubリポジトリ連携で画像をアップロードする方法)
では, 内部リンクの処理と同じように, ![ALT](https://zenn.dev/qawatake/images/sample.png)
の形式に変換すればいいのでしょうか?
答えは否です.
実際にやってみれば分かるのですが, 画像は https://zenn.dev/qawatake/images/
以下で配信されているのではなく, Cloudinary から配信されています.
その URL は乱数を含み, 事前に予測することはできません.
正しい形式は, 公式に GitHubリポジトリ連携で画像をアップロードする方法 で説明されている通り, ![ALT](/images/sample.png)
のようにプロジェクトのルートディレクトリからの絶対パスを使ったものです.
このように, 参照先が markdown ファイルなのか画像ファイルなのかでリンクの変換形式が異なるのが厄介なポイントです.
アンカーリンクの変換
Hugo も Zenn も, ヘディング H1, H2, ... に id を自動的に付与し, ページ内リンクを利用できます.
しかし, id の作成ルールは Hugo と Zenn で異なります.
例えば, Hugo では # Obsidian (オブシディアン) 😗
に付与される id は
obsidian-オブシディアン-
となり, ()
や 😗
が省略されます.
一方, Zenn では obsidian-(オブシディアン)-😗
となり, ()
や 😗
は省略されません.
Zenn では markdown → HTML の変換に markdown-it を使っており, 特にアンカーリンクの処理には, markdown-it-anchor を使っているようです.
ということで, markdown-it-anchor のソースコード にある通り,
const slugify = (s) => encodeURIComponent(String(s).trim().toLowerCase().replace(/\s+/g, '-'))
に相当する変換処理が必要そうです.
ちなみに, これは余談ですが, Obsidian でページ内リンクを入力しようとすると自動補完が入りますが, その際, カッコ()
を空白で置き換えてしまいます.
これでは Hugo でも Zenn でも, ページ内リンクが無効になってしまいます.
注意が必要です.
やりかた
使う道具
Zenn CLI と obsdconv (手前味噌ですが) を使います.
obsdconv は Obsidain でとったノートを他の形式の markdown ファイルに変換するために作成したプログラムです.
もともと Obsidian → Hugo のエクスポートに使うことを目的に作成しましたが, Zenn へのエクスポートにも使えるように改造しました.
実行するコマンド全部
Zenn のプロジェクトディレクトリのルートで ↓ を実行します.
SRC=/path/to/obsidian-vault
TMP=tmp
obsdconv -src $SRC -tgt "${SRC}/public" -dst $TMP \
-title -cptag -remapkey=tags:topics \
-rmtag -link -cmmt -rmh1 -formatLink -formatAnchor=markdownit \
-remapPathPrefix='public/notes/>https://zenn.dev/qawatake/articles/|public/static/>/images/' \
-filter='zenn&&published' -strictref
mv "${TMP}/notes/"* articles/
mv "${TMP}/static/"* images/
rm -r ${TMP}
ファイルは, 一旦, tmp
ディレクトリに出力して, その後, articles/
と images/
に移動しています.
↓ では obsdconv のオプションを説明します.
コマンドの説明
エクスポート先・エクスポート元の設定
-src
Vault のルートのパスを指定します.
より正確には, リンクを解決する際のルートディレクトリのパスを指定します.
つまり, ↑ の Vault 構造で言えば, GHI
は private/notes/GHI.md
のように解決されることになります.
-tgt
エクスポート元の markdown ファイルあるいはディレクトリを指定します.
-tgt
が指定されていなければ, -src
で指定したものをエクスポート元として使用します.
-dst
エクスポート先のディレクトリを指定します.
Front matter の処理
-title
本文から一番最初にあるヘディング H1 の内容を取得し, それを title
フィールドにコピーします.
-cptag
本文から #obsidian
のような埋め込みタグを取得し, それを tags
フィールドにコピーします.
-remapkey
front matter のキーの名前変更を行います.
今回は, -remapkey=tags:topics
と表記しているので, キー tags
が キー topics
にすげ替えられます.
注意点が3点あります.
1つ目は, 複数の変換を行う場合には, ,
で区切るということです.
例えば, -remapkey=tags:topics,publish:published
のように書きます.
2つ目は, 削除したいキーは右辺を空欄にすることです.
例えば, -remapkey=toBeDeleted:
のように書きます.
3つ目は, この処理は -title
や -cptag
の後に適用されるということです.
なので, -cptag
と -remapkey=tags:topics
を同時に指定すると, 「本文から埋め込みタグを取得して, topics
フィールドにコピーする」という動きになります.
本文の処理
-rmtag
本文から #obsidian
のような埋め込みタグを削除します.
同時に -cptag
が指定されている場合には, 先に -cptag
によって内容が取得されます.
-link
リンク周りの変換を行います.
代表的な変換は ↓ です.
-
[[sample | サンプル]]
→[サンプル](path/to/sample.md)
, -
[サンプル](sample)
→[サンプル](path/to/sample.md)
, -
[[sample#section]]
->[sample > section](path/to/sample.md#section)
, -
[[#section]]
→[section](#section)
, -
![[sample.png | サンプル画像]]
→![サンプル画像](path/to/sample.md)
.
-cmmt
Obsidian 独自のコメントブロック (%%コメント%%
) を削除します.
-rmh1
ヘッディング H1 を削除します.
-formatLink
リンクの拡張子 .md
をとったり, アンカーだけのリンク (#section
) にパスを補完したりします.
-link
と組み合わせて使います.
例↓
-
[[sample | サンプル]]
->[サンプル](path/to/sample)
-
[[#section | サンプル]]
->[サンプル](path/to/sample#section)
-formatAnchor
アンカーリンクのフォーマット形式を指定します. (アンカーリンクの変換)
デフォルトでは Hugo と同じ形式をとります.
今回は, Zenn へのエクスポートということで, markdown-it の形式を使います.
-formatAnchor=markdownit
と指定すれば大丈夫です.
-remapPathPrefix
リンクのパスのプリフィックスを変換してくれます.
-link
と組み合わせて使います.
remapPathPrefix='public/notes/>https://zenn.dev/qawatake/articles/|public/static/>/images/'
を指定した場合, ↓のように変換されることになります.
-
public/notes/sample.md
→https://zenn.dev/qawatake/articles/sample.md
-
public/static/sample.png
→/images/sample.png
このように, 変換前のプリフィクス>変換後のプリフィックス
の形式で記載し, 区切り文字には |
を使用します.
注意点は, ファイル自体の位置は変わらないということです.
つまり, ↑ で言えば, sample.md
はエクスポート先のディレクトリから始めて public/notes/sample.md
の位置に配置されたままです.
ファイル自体の位置を変えたければ, 別途 UNIX コマンドなどを使う必要があります.
エクスポートの条件設定
-filter
front matter のキーを使った条件式を指定して, true
となるファイルだけ出力します.
-filter='zenn&&published'
の場合は, zenn: true
かつ published: true
のノートだけがエクスポートの対象になります.
条件式には, &&
の他に OR ||
, NOT !
, カッコ ()
を使用できます.
条件式は ''
あるいは ""
で囲うことに注意です.
また, -remapkey
を同時に使用する場合, -remapkey
を適用した後に評価されます.
-strictref
内部リンクの先が見つからなかった場合に, エラーを吐いて処理を停止します.
ただちょっと実装が中途半端で, ↑ の -filter
を考慮してくれません.
なので, -filter
で取り除かれたファイルへの参照を禁止したい場合には, 一旦, obsdconv -filter...
でフィルタしてから, もう一度 obsdconv で変換するといった2段階の処理をする必要があります.
私と少し違う状況では...
私の Vault の管理方法は Zenn にエクスポートしやすい形でした.
管理方法が違うと ↑ のやり方にもう少し手を加える必要があります.
ノートが複数のフォルダに分かれている
Zenn では articles
以下は1階層しか許されていないようです. (2022/02/21 現在.)
なので, ファイルの出力先とリンクの変換には注意が必要です.
例として, ↓ のような Vault の構造を考えてみます.
.
|-- A
|-- abc.md
|-- B
|-- def.md
まず, ファイルの出力先ですが, これは UNIX コマンドなり何なりで対応します.
mv tmp/A/* articles/
mv tmp/B/* aritcles/
次に, リンクの変換ですが, -remapPathPrefix
を使用します.
-remapPathPrefix='A/>https://zenn.dev/qawatake/articles/|B/>https://zenn.dev/qawatake/articles/'
もし, A/
と B/
に同名のファイルがある場合には, これは Zenn のルールにそぐわないので, どちらかのファイル名を変えるしかありません.
ファイル名に日本語が入っている
Zenn ではスラグに日本語は使えません (Zenn CLIで記事・本を管理する方法).
なので, ファイル名をルールに沿う形に変更する必要があります.
最後に
Zenn CLI と obsdconv を使って Obsidian でとったノートを Zenn にエクスポートする方法を説明しました.
Zenn が markdown で記事を執筆し, GitHub を通してアップロードできる仕組みであるおかげで, こういった機械的な記事のエクスポートが可能になっています.
また, 2022/02/21 時点では, 画像のアップロードも images/
以下に配置すれば勝手にやってくれるので, これまたエクスポートのハードルを下げてくれています.
ありがたいことです.
Discussion