Obsidian:内部ノートをブログカード表示にする

ツール活用

WordPressを始めとするブログ記事内で、外部リンクの一例として使われているブログカードは実に見栄えが良い。

Obsidianでは Auto Card Link プラグインで同様のブログカード表示が可能だが、基本的に外部URLに対してで、Vault内のローカルノートでは使えない。

そこで同じような見栄えのものが作れないかを試行錯誤していて、ようやく満足ゆく結果に辿り着いた。これはその成果です。

代用の試行錯誤

Obsidian:WebClipを画像サムネ付きにしてみた - Jazzと読書の日々
これはこれでインターネット・データベース。 WebClip Obsidian Web Clipperのクリップを閲覧するdataviewスクリプト。 前回ArcSearch用クリッパーを作ったことで「サムネがつくと視認性が上がる」と気づき改...
📘Obsidian WebClipperを導入してみた
はじめに 最近Obsidian界隈で話題のObsidian Web Clipperを自分好みにして導入してみた。 あくまでメモを取る機能とは別であるので、全く別のブロックとして機能させることとした。 Chrome拡張機能の設定 titleを...

ちょうど同時期に内部リンクをグラフィカルに表示する例を見かけたので、まずは Dataview や QuickAdd プラグインで作成できないかを自分なりに頑張ってみた。

Dataview でここまでは作れたものの、満足する域にはほど遠い。
アプローチをリセットし、改めてゼロから模索してみることにした。

我々が2000年前に通過した場所だ

烈海王「キサマ等の居る場所は既に我々が2000年前に通過した場所だ」

改めて理想とする Auto Card Link プラグインの機能詳細と、fork派生したものを調査してみた。

すると、元祖プラグインの時点でローカル画像の表示には対応していた!
さらに外部URLについても「URI形式なら通るのではないか?」という思いつきで、Obsidian URIを試したら、通った!

つまり、Auto Card Link プラグインは元から「ローカル画像をサムネイル表示して、ローカルノートへのリンクをブログカード表示する」ことができていたのだった。

Obsidian URI は、タブ上右クリックで表示されるメニューから取得可能

ローカルノートをご家庭でも簡単にブログカードにできるんですよ(土井善晴調)

Vault内ローカルノートをブログカード形式で表示することはできた。
あとはそれを以下に簡単にノートに埋め込みできるか。

Templaterプラグインでの差し込みを検討すべく、ChatGPT君に以下の内容で相談してみた。

# 目的
- アプリ「Obsidian」に機能を追加します。
- 現在開いているノートに、別のノートへのリンクを追加します。
- リンクを追加すると同時に、そのリンク先のノートのthumbnail画像、Obsidian URI、descriptionをリンクに続けて追記します。

# アクションの流れ
1. リンクしたい対象ノートを選ぶ
2. 対象ノートのタイトルを取得
3. 対象ノートのプロパティ「thumbnail」のファイルパスを取得
4. thumbnailのアドレスがない場合は、ランダム画像サイトのURLに置換
5. 対象ノートのObsidian URIを取得
6. 対象ノートの全文から、タグ等を除く先頭100文字を「description」として取得
7. リンクを貼る場所に2~6の要素毎に改行して出力

上記の時点でかなり正解に近い答えを提示してくれたが、そのあとも細部調整や好みの取り入れ、様々なパターン対応をして、以下の Templater スクリプト「Local Card Link」が完成しました。

<%*
const dv = app.plugins.plugins.dataview.api;

// Vault内のすべてのノートを取得
const files = app.vault.getMarkdownFiles();

// 更新日の新しい順にソート
const sortedFiles = files
    .map(f => ({
        path: f.path,
        mtime: f.stat.mtime, // 更新日(UNIXタイムスタンプ)
    }))
    .sort((a, b) => b.mtime - a.mtime); // 新しい順にソート

// ファイルパスだけのリストを作成
const fileList = sortedFiles.map(f => f.path);

// プロンプトでリストからノートを選択
const file = await tp.system.suggester(fileList, fileList);
if (!file) {
    tR += "ノートが選択されていません。";
    return;
}

// 対象ノートのメタデータを取得
// プロパティの存在しないノートにも対応
const note = dv.page(file);
const title = note?.file?.name || "Untitled";
const creationDate = note?.file?.ctime || Date.now();
const vaultName = app.vault.getName();
const encodedFileName = encodeURIComponent(note?.file?.path || file);
const obsidianUri = `obsidian://open?vault=${encodeURIComponent(vaultName)}&file=${encodedFileName}`;

// YAMLフロントマターまたは埋め込み情報からサムネイルを取得
const thumbnail = note?.thumbnail || note?.cover || note?.image || `https://picsum.photos/seed/${creationDate}/500/300`;

// 説明がなければ本文より生成
let description = note?.description || note?.excerpt || note?.extract || "";
if (description == "") {
    // ノート内容を取得して不要な部分を削除
    let content = await app.vault.adapter.read(file);

    // YAMLフロントマターを削除
    content = content.replace(/^---[\s\S]*?---/, "");

    // 埋め込みや画像リンクを削除、文中ノートリンクはノート名を残す
    content = content.replace(/(!?\[\[.*?\]\])\n/g, ''); // Wikilink
    content = content.replace(/(!?\[.*?\]\(.*?\))\n/g, '');
    content = content.replace(/\[\[(.*?)\]\]/g, '$1'); // Wikilink
    content = content.replace(/\[(.*?)\]\(.*?\)/g, '$1');

    // その他マークダウン記法箇所を削除
    content = content.replace(/^\\/mg, ''); // エラー回避
    content = content.replace(/<[^>]*?>/g, ''); // HTMLタグ
    content = content.replace(/[\*\-\_]{3,}/mg, ''); // 罫線
    content = content.replace(/```[\s\S]*?```/g, ''); // pre記法
    content = content.replace(/^\-\s\[(.*?)$/mg, ''); // タスク
    content = content.replace(/^>+\s(.*?)$/mg, ''); // 引用
    content = content.replace(/^[\t\s]+?\s(.*?)$/mg, ''); // 段落下げ
    // 以下はマークダウン記法のみ除去または本文圧縮
    content = content.replace(/ /g, ' '); // 全角空白
    content = content.replace(/  /g, ' '); // 空白複数圧縮
    content = content.replace(/`([^`]*?)`/g, '$1'); // 文中code記法
    content = content.replace(/^\-\s(.*?)$/mg, '$1'); // 箇条書きリスト
    content = content.replace(/^\d+?\.\s(.*?)$/mg, '$1'); // 番号リスト
    content = content.replace(/[\*\_]{1,}(.*?)[\*\_]{1,}/g, '$1'); // 強調等  
    content = content.replace(/\n\s*\n/g, '\n'); // 複数改行

    // # 見出しとその直後の内容を抽出
    const summaryMatch = content.match(/^(?:#{1,})\s.+\n([\s\S]*?)(?=^#{1,}\s|$)/m);

    if (summaryMatch && summaryMatch[1] && summaryMatch[1].length > 20) {
        // 見出しを除き、直後のテキスト部分だけ取得
        description = summaryMatch[1].trim();
    } else {
        // 該当がなければノート全体から取得
        content = content.replace(/^#{1,}\s.+?$/mg, '');
        description = content;
    }
}

// 説明を先頭70文字以内に収める
description = description.slice(0, 70).trim();
description = description.replace(/\n/g, ' ')

// リンクを現在のノートに追加
tR += `
\`\`\`cardlink
url: ${obsidianUri}
title: "${title}"
description: "${description}..."
host: Local Vault
favicon: https://publish.obsidian.md/favicon.ico
image: "[[${thumbnail}]]"
\`\`\`
`;
%>

完成スクリプトの出力例

スクリプトの仕様と改造ポイント

Templater プラグインは以下の仕様としています。

あくまで自分好みに整備しやすい形としたため、スクリプト本体や正規表現、出力結果についても、あなた好みに改造してください。

前提環境

  • Templaterプラグイン
  • Auto Card Linkプラグイン

スクリプト仕様

  • プロパティ(YAMLフロントマター)の無いノートにも対応
  • サムネイル画像は、プロパティ「thumbnail」「cover」「image 」のいずれかがあればそれを表示。なければpicsumからノート生成時間から基づいた固定画像をサムネイルとして利用
    • picsumが微妙なサイズを受け付けなくなっていたので、480x270 → 500x300に変更しました(2024-12-13追記)
  • ノートがサムネイル画像を含む場合、画像パスについては絶対パス推奨
  • ブログカードの説明文(description)は、プロパティ「description」「excerpt」「extract」のいずれかがあればそれを表示。なければノート本文から生成する。
  • 説明文には不要な内容を含みづらくするため、マークダウン記法やHTMLタグなどを取り除く処置に対応
  • favicon.ico は Obsidian URL から引っ張ってきているため、不要なら favicon 列を削除
  • ノートにブログカードを生成して差し込む形のため、Obsidian URIについては生成時のリンク先のパスになります。そのため、リンク先を移動しても通常のリンクのように自動変更されません(2024-11-21追記)
  • スクリプト実行時に表示されるリンク先候補について、更新日順に並ぶようスクリプト前半を変更しました(2024-11-23追記)

利便性を高める

Templater プラグインが完成したら、ホットキー割当にしたり、CommanderプラグインやSlash commanderプラグインで、呼び出しの効率化を図ります。

私の場合は「Ctrl + L」で呼び出せるよう、ホットキー割り当てしました。

 
*Top Image by Stable Diffusion