0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Obsidianプラグインで自分だけの日本酒データベースを作った

0
Posted at

1. はじめに

お盆休みの3日間を使って何か作って公開してみよう!という個人チャレンジです。
もともと日本酒が好きで、飲んだ日本酒の特徴を手軽に記録しておきたいという思いから最終的にObsidianのプラグインとして今回機能を実装しました。
できあがったものは、次のようなイメージです。

sake-log.png

動作環境

  • Obsidian: 1.13.4
  • obsidian(npmパッケージ/型定義): 1.12.3
  • TypeScript: 5.9.3
  • Node.js: 20.11.0

この記事では、Obsidianプラグイン開発に興味がある方向けに、Obsidianプラグインの特徴や実際に手を動かして学んだ知見を中心に紹介します。細かな実装についてはプラグインのサンプルコードを見られることをお勧めします。
https://github.com/obsidianmd/obsidian-sample-plugin

2. なぜObsidianを選んだのか

企画の当初は、クラウドサービス(Supabase、Firebase、AWSなど)にデータを保存するWebアプリ案も検討しました。個人開発であれば、認証やストレージが最短で揃うこれらのサービスは、実装の速さという点で魅力的です。

ただ、今回はせっかく作るなら、自分だけでなくいろいろな人に使ってもらえるアプリにしたいという思いがありました。そう考えたときに引っかかったのが、クラウドサービスを選ぶと、ユーザーが記録したデータの永続性を、自分(開発者)が握ってしまうという点です。もし自分がこのアプリのユーザーだったら、開発者の気分ひとつ長年自分が貯めてきた日本酒の記録が消えてしまうというのは不安だなと思い、自分の手元にデータを残せるObsidianを今回選択しました。
またObsidianで記録することにより、あとから自由に評価を追加したり、日本酒の記録と自分の日記をリンクさせたり、Obsidian上でユーザーが自分のデータを自分の好きなように扱えることもObsidianに記録を残すメリットだと思っています。

3. 記録のハードルを下げる:入力のしやすさと一覧性を両立する

日本酒ログは、たいてい飲みながらメモを取ります。凝った入力を求めると、結局面倒になって続かなくなってしまいます。そこで、今回のフォーム設計では、できるだけ手軽に、直感的に記録できることを最優先にしました。

必須項目は銘柄と味わいだけにする

フォームの必須項目は、銘柄名味わい(甘辛度・淡麗芳醇度) の2つだけにしています。蔵元や購入店、価格といった項目はすべて任意です。飲んでいるその場で、まず記録しておきたいのはこの2つだけだろう、という考えからです。

味わいについても、数値をキーボードで入力させるのではなく、正方形のエリアの中でピンをドラッグするだけで決まる形にしました。

  • 横軸:甘口 ←→ 辛口
  • 縦軸:淡麗 ←→ 芳醇
  • 初期位置:中央(0, 0)

「今回はいつもよりちょっと辛口寄りだったな」という感覚を、数値を意識せずそのままタップ&ドラッグの動きに落とし込める入力方式です。厳密な評価というより、飲みながらでも片手でサッと済ませられることを重視しています。

一覧で見返せるように、メモもプロパティに入れる

記録したノートは、あとからまとめて見返したくなります。今回はObsidianのBases機能で一覧表示することを想定していますが、Basesが一覧・フィルタの対象にできるのは、ノート本文ではなくfrontmatterのプロパティだけです。

そのため、自由記述のメモも含めて、記録内容はすべてfrontmatterのプロパティとして保存する形にしました。

---
brand: "獺祭"
sweetness: 2.3
richness: -1.5
brewery: "旭酒造"
date: 2026-08-13
place: "地元の酒屋"
price: 1500
cover: "sake-log/sake-photos/2026-08-13_獺祭.jpg"
memo: "フルーティーで香り高い。冷やして飲むのがおすすめ。"
tags: [sake-log]
---

メモのような複数行になりうるテキストをプロパティに入れる場合、改行の扱いに一工夫が必要になります。この実装上の細かい処理は5章で紹介します。

4. Obsidianプラグインの基本の「き」

Markdownファイルを書き出す等の機能を作る以前に、まず「Obsidianプラグインとして動く」状態にする必要があります。このあたりがWebアプリとの大きな違いでした。

起動のきっかけがまったく違う

普通のWebアプリは、対象のURLを叩くことが起動のきっかけになります。ところがObsidianプラグインでは、この前提が成り立ちません。かわりに、コマンドパレットからのコマンド実行や、左サイドバーのリボンアイコンのクリック、設定画面を開く操作などが起動のきっかけになります。

そして、これらのきっかけは、あらかじめObsidian本体に対して「このコマンドを使います」「このアイコンをリボンに置きます」と登録して申告しておかなければなりません。URLを叩けばアプリが起動するWebアプリとは異なり、Obsidianプラグインは自分から名乗り出るまで、Obsidianにその存在を認識してもらえないのです。

最低限、何を登録すればよいか

今回のプラグインでは、main.tsonload()の中で、次の3つを登録しています。プラグインが読み込まれると、このonload()が実行されます。

src/main.ts
import { addIcon, Plugin } from 'obsidian';

export default class SakeLogPlugin extends Plugin {
	settings!: SakeLogSettings;

	async onload() {
		await this.loadSettings();

		addIcon(SAKE_BOTTLE_ICON_ID, SAKE_BOTTLE_ICON_SVG);
		this.addRibbonIcon(SAKE_BOTTLE_ICON_ID, '日本酒ログを記録', () =>
			openSakeLogModal(this.app, this.settings),
		);

		this.addCommand({
			id: 'open-entry-form',
			name: '記録を追加',
			callback: () => openSakeLogModal(this.app, this.settings),
		});

		this.addSettingTab(new SakeLogSettingTab(this.app, this));
	}
}
  • addRibbonIcon():左サイドバーにアイコンを追加し、クリック時の処理を紐づけます
  • addCommand():コマンドパレット(Ctrl+P)から呼び出せるコマンドを登録します
  • addSettingTab():プラグインの設定画面を追加します

これで、クリックされたときやコマンドが実行されたときに何かが起きるか(登録画面のモーダルを開く等)ができるようになりました。モーダルの中身などは通常のwebアプリと変わりませんのでここでは割愛させていただきます。

Pluginの公開

Obsidianのプラグインの実体はGithub上の下記3ファイルです。開発時は TypeScriptで書きますが、最終的に読み込まれるファイルは下記3つ。

main.js
manifest.json
styles.css

これらのファイルを元に下記の手順を踏むとPluginとして公開できます。
※詳しくはこちらの記事参照: Obsidianコミュニティプラグインを公開してみる
①GitHubにアップロード
②リリースを作る
③コミュニティプラグインとしてのレビューを依頼、通過する

配布にも2つの選択肢がある

プラグインが動くようになっても、それを人に届ける方法は1つではありません。上記のObsidianの正式なコミュニティプラグイン一覧に載せるには、GitHub上での審査を経る必要があり、数週間単位の時間がかかります。3日間で公開まで終えたかった今回は、この方法は選びませんでした。

代わりに使ったのが、BRAT(Beta Reviewer's Auto-update Tool)です。BRATはObsidianのコミュニティプラグインの1つで、公式審査を経ていない開発中のプラグインでも、GitHubリポジトリのURLを指定するだけでインストールできるようにする補助ツールです。(上記の②までのステップでプラグインを使える状態にできる)個人開発・実験段階のプラグインを試しに使ってもらう手段として広く使われており、正式リリース前でも公開して人に使ってもらえる状態にすることができました。

5. 実装上の工夫:フォーム入力をMarkdownとして書き出す

Obsidianプラグインとして動く土台ができたら、次はフォームで入力した内容をMarkdownファイルとして保存する処理ですが、いくつか気を付けないといけに点があったのでご紹介です。

保存先フォルダが存在するとは限らない

Obsidianのvault.createFolder()は、親フォルダがすでに存在していないとエラーになります。プラグインを初めて使うユーザーは、当然「日本酒ログ」用のフォルダをまだ作っていません。フォルダがない状態で保存しようとしてエラーが出てしまったため、パスを/で分割し、存在しない階層を順番に作る処理を挟みました。

src/utils/vaultFolder.ts
import { normalizePath, Vault } from 'obsidian';

export async function ensureFolder(vault: Vault, path: string): Promise<void> {
	const segments = normalizePath(path)
		.split('/')
		.filter((segment) => segment.length > 0);

	let current = '';
	for (const segment of segments) {
		current = current ? `${current}/${segment}` : segment;
		if (!vault.getFolderByPath(current)) {
			await vault.createFolder(current);
		}
	}
}
  • normalizePath()は、パスの表記ゆれ(先頭・末尾のスラッシュ、連続するスラッシュなど)をObsidianの内部表現に統一するユーティリティ関数です
  • split('/')でパスを階層ごとの配列に分解します。例えばsake-log/2026なら['sake-log', '2026']になります
  • 階層を1つずつ辿りながらcurrentにパスを積み上げ、vault.getFolderByPath()で存在確認したうえで、なければvault.createFolder()で作成します

「浅い階層から順番に、なければ作る」を繰り返すことで、どんなパスを渡されても安全にフォルダを用意できます。これは日本酒ログに限らず、ユーザーが指定したパスへ動的にファイルを保存するタイプのプラグイン全般で必要になる処理だと思います。

同じファイル名が既にあるかもしれない

同じ日に同じ銘柄を2回記録する、といったケースを考えていなかったため、最初のバージョンでは既存ファイルを気づかず上書きしてしまう実装になっていました。vault.getAbstractFileByPath()で事前に存在を確認し、衝突する場合だけ末尾に連番を付ける形に直しました。

src/utils/saveSakeLogEntry.ts
import { normalizePath, Vault } from 'obsidian';

async function resolveUniquePath(
	vault: Vault,
	folder: string,
	baseName: string,
	extension: string,
): Promise<string> {
	let candidate = normalizePath(`${folder}/${baseName}.${extension}`);
	let suffix = 2;
	while (vault.getAbstractFileByPath(candidate)) {
		candidate = normalizePath(`${folder}/${baseName} (${suffix}).${extension}`);
		suffix += 1;
	}
	return candidate;
}
  • まずは連番なしの素直なパス(例:sake-log/2026-08-13_獺祭.md)を候補として用意します
  • vault.getAbstractFileByPath()は、指定したパスにファイルが存在すればそのオブジェクトを、存在しなければnullを返します。これをwhile文の条件に使い、存在する間はsuffixを1つずつ増やしながら候補を作り直します

「ファイルを保存する」という処理には、常に「同名ファイルがあったらどうするか」という問いがセットでついてきます。これも今回に限らず汎用的に使えるtipsです。

複数行のメモをfrontmatterに入れる

自由メモの欄は、改行を含む複数行の文章が入力されます。ところが、3章で触れたとおり、Basesが一覧・フィルタの対象にできるのは本文ではなくfrontmatterのプロパティだけです。メモも一覧で見返せるようにしたかったため、複数行のテキストを\nという文字列にエスケープし、frontmatter上は1行のYAML文字列として保持する形にしました。

src/utils/frontmatter.ts
function yamlString(value: string): string {
	const escaped = value
		.replace(/\\/g, '\\\\')
		.replace(/"/g, '\\"')
		.replace(/\r\n/g, '\\n')
		.replace(/\n/g, '\\n');
	return `"${escaped}"`;
}

改行入りのテキストをfrontmatterプロパティとして保存したい場合、この方法が参考になると思います。

6. 実際に使ってみる

このプラグインは、Obsidianの正式なコミュニティプラグイン一覧には未掲載ですが、BRATを使うことで、審査を待たずに今すぐ試せます。

インストール手順

  1. Obsidianの設定から「コミュニティプラグイン」を開き、BRATを検索してインストール・有効化する
  2. BRATの設定画面を開き、「Add Beta plugin」を選択する
  3. 以下のリポジトリURLを入力する
    1. 「Add Plugin」を押すとインストールされるので、Obsidianのコミュニティプラグイン一覧から有効化する
https://github.com/nekotetsu/sake-log-obsidian

使い方

  • 左サイドバーの日本酒瓶アイコン、またはコマンドパレットから「記録を追加」を実行すると、登録画面が開きます
  • 銘柄名と味わい(甘辛度・淡麗芳醇度)だけ入力すれば記録できます。他の項目は任意です
  • 設定画面から、ノートと写真の保存先フォルダを変更できます

7. 振り返り:3日間やってみて

今回、初めて「自分ひとりが使えればいい」ではなく「他の人にも使ってもらう」ことを前提にアプリを作ってみて、考える視点が大きく変わることに気づきました。データの永続性や、ユーザーが自分のデータをどれだけ自由に扱えるかといった観点は、自分専用のツールを作るだけなら、そこまで深く考えなかったと思います。「もし自分がユーザーだったら」という視点を持つことで、はじめてクラウド頼りではなくObsidianのvaultにMarkdownとして残すという判断にたどり着けました。

機能面では、当初やりたかったこと(手軽な記録、味わいの直感的な入力、あとから一覧で見返せること)は、3日間という制約の中でも一通り実装できたと思っています。すでに自分自身で使い始めていて、しばらくはこのまま日本酒ログとして活用していくつもりです。

もしこの記事を読んでObsidianプラグイン開発に興味を持ってもらえたり、実際にこのプラグインを試してもらえたりすれば、それ以上に励みになることはありません。

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?