😗

Obsidian → Zenn の運用方法

2022/02/23に公開

ざっくり

最近, 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/*.mdarticles/*.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 で管理しています.
なので, tagstopics というようにキーを書き換える必要があります.

内部リンクの処理

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 CLIobsdconv (手前味噌ですが) を使います.

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 構造で言えば, GHIprivate/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.mdhttps://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 CLIobsdconv を使って Obsidian でとったノートを Zenn にエクスポートする方法を説明しました.
Zenn が markdown で記事を執筆し, GitHub を通してアップロードできる仕組みであるおかげで, こういった機械的な記事のエクスポートが可能になっています.
また, 2022/02/21 時点では, 画像のアップロードも images/ 以下に配置すれば勝手にやってくれるので, これまたエクスポートのハードルを下げてくれています.
ありがたいことです.

Discussion