Obsidianで扱うファイル名をUIDにする

Posted on Jan 21, 2024

Obsidianを使うにあたり、 ノートのタイトルがそのままファイル名として使われることに、主に以下の2点について不便を感じていた。

  • タイトルを変更するとリンクが変わってしまう
  • 日本語のファイル名はターミナルから扱いにくい

自分はノートを雑に取っていくスタイルなので、 ノートの中身を書いている途中でタイトルを変えたくなることがしょっちゅうある。 Obsidian管理下のリンクであれば自動で更新してくれるので問題ないのだが、 Obsidian外にファイルパスを記録している場合に、タイトルを変更するとリンク切れになってしまうので困る。

ターミナルからの日本語ファイル名操作は言わずもがな。

と、いうことでUID的なものをファイル名を使うために試行錯誤したという記録。

Obsidian Advent Calender 2023 にあった Obsidianのノートのファイル名をUID化してZettelkastenメソッドを実現する - My Zettelkasten Notes を大いに参考にさせてもらった。

やったこと

  • mediawiki方式のリンクを無効化
  • コミュニティプラグイン Templater 用のテンプレート作成
    • 新規作成用のテンプレートを用意
      • ノート作成後に YYYY/MM/YYYYMMDD_HHmmss にファイルを移動する
    • テンプレートの選択からfrontmatterの合成まで全部コードで書いた
      • すべて新規作成用テンプレート経由で、そこから各テンプレートタイプを選択する方式
      • 各タイプ別テンプレートを直接呼び出す場合、リネームがうまく動かなかった
  • コアプラグイン Daily notes の設定
    • Date formatYYYY/MM/YYYYMMDD
      • Templaterがrenameするパスに合わせる
      • リネームをTemplaterに任せると、Daily note作成→新規ノート作成→リネームしようとしてファイル名重複 となってしまうので、最初からリネーム後の名前を使う
    • 時刻部分がないもの = Daily notes という扱いにした
  • コミュニティプラグイン Front Matter Header 導入
    • 各所でのノートタイトル表示を、Frontmatter内の要素に置き換え
    • 設定の Features は全部有効化
    • 設定の blacklist に各プラグインから使用するノート(テンプレートファイルとか)が入っている _plugin ディレクトリを追加

やらなかったこと

  • コアプラグイン Unique note creator 導入
    • Templaterとの組み合わせが上手くいかなかった
      • 特にtitleを入力する部分

もうちょっと詳しく

Templater のテンプレート

以下のようなTemplaterテンプレートを作成し、 /Folder Template として設定した。

<%*
const uid = tp.date.now("YYYYMMDD_HHmmss");

const extractByHeader = async (path, header) => {
    let content = await tp.file.include(`[[${path}#${header}]]`);
    if (!content.startsWith(`# ${header}`)) {
        return undefined;
    }

    content = content.replace(new RegExp(`^# ${header}\n+`), "");
    return content.trim();
};

// select template

const templateName = await tp.system.suggester((item) => item, [
    "plain",
    "idea",
    "person",
    "review book",
    "review movie",
]);

const templateDir = "_plugin/templater/type";
const templateFile = tp.file.find_tfile(`${templateDir}/${templateName}`);

// extract template frontmatter

let templateFM = {};
await app.fileManager.processFrontMatter(templateFile, (fm) => {
    templateFM = Object.assign(fm, templateFM);
});

// extract template parts

const templateTitle = await extractByHeader(templateFile.path, "%title%");
const templateContent = await extractByHeader(templateFile.path, "%content%");

// set title

let title = tp.file.title;
if (templateTitle !== undefined) {
    title = templateTitle;
}
else if (title.startsWith("Untitled")) {
    title = await tp.system.prompt("Title");
}
-%>
---
uid: "<% uid %>"
title: "<% title %>"
aliases: []
created_at: <% tp.date.now("YYYY-MM-DD HH:mm:ss") %>
archived_at: null
<%*
for (const [key, value] of Object.entries(templateFM)) {
    tR += `${key}: ${JSON.stringify(value)}\n`;
}
-%>
---
<% templateContent || "" %>
<%*
// rename
await tp.file.move(`${tp.date.now("YYYY/MM")}/${uid}`);
-%>

おそらく特徴的なのは tp.system.suggester でテンプレートタイプを選択し、 対象テンプレートから extractByHeader 関数で値を取ってくる部分。 ファイル名パターンの定義とFrontmatterを共通化するために、用途別要素はタイプごとに用意したノートに定義している。

---
tags:
  - idea
---
# %content%

## What
-
## Why
-
## How
-

# %{パラメータ名}% の部分が特別な見出しという扱いで、この見出しは以下の値をメインのテンプレートから参照している。 タイプ別ノートにFrontmatterがあればそれも埋め込む。

リネーム部分をincludeする方式はうまく行かなかった

はじめはファイル操作などの共通部分だけ分割し、 用途ごとのテンプレートから tp.file.include で共通処理をインクルードする方式を試していた。

<%*
// こういう内容のテンプレートを作り、
await tp.file.move(`${tp.date.now("YYYY/MM/YYYYMMDD")}`);
-%>
<%*
// 別のテンプレートからincludeする
tp.file.include("path/to/rename.md")
>

この場合 Templater: create new note from template コマンドでテンプレートを呼び出すと、 ファイルリネームのタイミングのほうがノートタイトルの入力よりも先になってしまい、 入力したタイトルがテンプレート内から参照できない、という問題があった (おそらくテンプレート処理の実行タイミングの問題だと思われる)。

Daily notes は専用テンプレート

Daily notesは命名パターンを YYYY/MM/YYYYMMDDとしたかったので、別テンプレートとした。 参考にした記事に倣うと YYYY/MM/YYYYMMDD_000000 というパターンになるが、 0時0分0秒にノートを作る可能性は0ではないなーと思ったので。 Daily notesの設定で Date format を上記のパターンを指定するだけ。

テンプレートタイプを選択する必要がないので、メインのテンプレートとは分けたというのもある。

現状の課題

Memosからのリンク補完

思いついたことをさっとメモする用途として、 Obsidian Memos を使用している。 Memosの入力欄で [[ と入力した場合、 Front Matter Title の効果が及ばず、ファイル名しか補完候補として表示されない。 既存のノートに関連するメモを残したいときに不便。

リンク先が存在しないリンクの挙動

例えばまだノートが存在しない「生成AI」というキーワードをとりあえずリンクとして書いておいた場合、

[[生成AI]]

これをクリックするとノートは期待通りに作られるのだが、 リンクが以下のように書き換えられてしまう。

[[2024/01/20240121_150352]]

元のリンク名を残す方法は無いものか。

既存のノートのファイル名

ファイル名をUIDにする前の、日本語なファイル名のノートが大量にある。 一括で書き換えスクリプトを書いてもいいが、 ノート内リンクもまとめて書き換えないとリンク切れになってしまうのでどうしようかな~というところ。

Obsidia外から操作するよりも、たとえばTemplaterで操作したほうがトラブルが少ないかも?

おわり

ともあれやりたいことはできたので満足。

Obsidian、Dataview と Templater を使えば大抵のことはなんとかできるし、 より高度なことをしたければプラグインを書けばいい、というカスタマイズ性の高さがとても楽しい。