[[📒Articles]] > [[📒2021 Articles]]
![[2021-07-25.jpg|cover-picture]]
## はじめに
[[Templater]]を使って、[[Obsidian]]や[[Obsidian Publish]]にてカード形式のリンクを作成できるようにした話です。作ったリンクは以下のようなデザインになります。
<div class="link-card">
<div class="link-card-header">
<img src="https://avatars1.githubusercontent.com/u/9500018?s=460&v=4" class="link-card-site-icon"/>
<span class="link-card-site-name">MAMANのITブログ</span>
</div>
<div class="link-card-body">
<div class="link-card-content">
<div>
<p class="link-card-title">MAMANのITブログ</p>
</div>
<div class="link-card-description">
Webエンジニアによる エンジニアリング/業務効率化/ガジェット に関するブログ
</div>
</div>
<img src="https://avatars1.githubusercontent.com/u/9500018?s=460&v=4" class="link-card-image" height="50"/>
</div>
<a href="https://blog.mamansoft.net/"></a>
</div>
以下は[[Obsidian]]で挿入するときのイメージです。
![](https://github.com/tadashi-aikawa/obsidian-templater-insert-link-card/raw/main/demo.gif)
**実現方法にのみ** 興味のある方は[[#自分の Vault でも使えるように]]を読んでください。
## 経緯
最近、ブログを[[MAMANのITブログ]]から[[Obsidian Publish]]に移行しました。[[📰Obsidian Publishでブログ運営ができるか (2021年版)|📰Obsidian Publishでブログ運営ができるか]]を真剣に考え、決断までには時間がかかりましたが、当初の目的であった以下のメリットは享受できています。
- 記事を書くハードルが下がる
- どこでも記事を作成/修正できる
- 実際は修正が多い
- リンク切れの影響を受けず、関連Noteにリンクできる
- [[Obsidian]]でブログの情報を全文検索できる
一方で、懸念していたデメリットの1つに **見た目の表現力が落ちる** というものがありました。コードが多い内容ならまだしも、コードブロックが一切登場しない記事は文字が多くなってしまいます。これは記事全体のシルエットとしても美しくありません。
## カードリンクの効力
[[MAMANのITブログ]]で執筆をしていた頃から画像を多用していたわけではありません。それらを使うのは、キャプチャや[[GIF]]で説明が必要なケースくらいです。それでも、シルエットがそこまでおかしくなかったのはカードリンクの効力があったおかげでしょう。
![[Pasted image 20210725193411.png]]
[[MAMANのITブログ]]では紹介したリンク先をカード形式で表示していました。このカードは[[embed.ly]]というサービスで作成されたものです。
<div class="link-card">
<div class="link-card-header">
<img src="https://embed.ly/static/images/favicon.png?v=f1716" class="link-card-site-icon"/>
<span class="link-card-site-name">Embedly</span>
</div>
<div class="link-card-body">
<div class="link-card-content">
<div>
<p class="link-card-title">Embedly makes your content more engaging and easier to share | Embedly</p>
</div>
<div class="link-card-description">
Embedly delivers the ultra-fast, easy to use products and tools for richer sites and apps.
</div>
</div>
</div>
<a href="https://embed.ly/"></a>
</div>
文章の合間にカードリンクを入れることで華となります。[[Obsidian Publish]]でも同様の見た目を実現したいと思っていました。
## Obsidian Publishでは[[embed.ly]]が使えない
まずは[[embed.ly]]で作成される[[HTML]]をはめ込んでみました。しかし、[[HTML]]を使う場合は `<script>` タグで[[JavaScript]]のコードを読み込むことが前提となります。[[Obsidian]]や[[Obsidian Publish]]ではそれが実現できませんでした。[^1]
他にも同様のサービスを探してみましたが、今回の要件を満たすようなものは見つけられませんでした。
## [[Obsidianプラグイン]]を作るという選択肢
次に[[Obsidianプラグイン]]で実現する方法を考えました。以下のようなコードブロックを作成したらプレビュー画面でカードに展開されるイメージです。
````markdown
```card
https://blog.mamansoft.net/2021/02/14/create-auto-complete-plugin-for-obsidian/
```
````
いくつか懸念はあったものの実現できるとは思っていました。
- HTML取得、解析に時間がかかる件は結果をキャッシュしておく
- [[CORS]]は[[ObsidianプラグインやTemplaterでCORS制限を回避する方法]]を使う
しかし、[[Obsidian Publish]]での実現は以下の点で困難だと思いました。
- [[ObsidianプラグインやTemplaterでCORS制限を回避する方法]]が使えない [^2]
- パフォーマンスを求めるにはブラウザでのキャッシュコントロールが必須
**[[Obsidian Publishとの互換性を最優先]]**したいというのが私のポリシーです。そのため、[[Obsidian Publish]]で使えない仕組みを導入する気にはなれませんでした。
## [[Templater]]で[[HTML]]を出力する方針
エディタの見た目が賑やかになることは諦め、[[URL]]からカードの生[[HTML]]をつくる方針に切り替えました。はじめは[[Obsidianプラグイン]]で実現しようかと思っていましたが、要件からして[[Templater]]で実現できることに気付きました。
<div class="link-card">
<div class="link-card-header">
<img src="https://github.githubassets.com/favicons/favicon.svg" class="link-card-site-icon"/>
<span class="link-card-site-name">GitHub</span>
</div>
<div class="link-card-body">
<div class="link-card-content">
<div>
<p class="link-card-title">GitHub - SilentVoid13/Templater: A template plugin for obsidian</p>
</div>
<div class="link-card-description">
A template plugin for obsidian. Contribute to SilentVoid13/Templater development by creating an account on GitHub.
</div>
</div>
<img src="https://opengraph.githubassets.com/3a54907a0d10242790a436f94ce07ae7f7f70ecda374433fe67fc564f81f2804/SilentVoid13/Templater" class="link-card-image"/>
</div>
<a href="https://github.com/SilentVoid13/Templater"></a>
</div>
[[Obsidianプラグイン]]にした方が導入は楽だったかもしれません。ただ、以下の理由から[[Templater]]を使った方がいいと判断しました。
- 外向けコンテンツでしか使わない (今のところ [[📒Articles]])
- カスタマイズ([[JavaScript]]、[[CSS]])がプラグインよりは容易
- テーマやパターンの考慮が大変
- 不正なレスポンスを返す[[URL]]が存在する場合の対応が大変
- [[Rich Links]]というプラグインがもうすぐ公開されそう
**自分が実現したいことに対するプラグインメンテナンスの費用対効果が割に合わないと思ったから** です。
## 必要な情報取得のロジック
無料のアカウント登録で公開されている[[REST API]]に限っていうと、カードリンクの作成に必要な情報を取得できるAPIを私は知りませんでした。そのため、ロジックは自分で作成しました。[^3]
```javascript:main.js
function trimMax(str, num) {
return str.length > num ? `${str.substring(0, num)}...` : str;
}
async function fetchAsDom(url) {
const res = await request({ url });
return new DOMParser().parseFromString(res, "text/html");
}
function getMetaByProperty(dom, property) {
return dom.querySelector(`meta[property='${property}']`)?.attributes?.content
?.value;
}
function getMetaByName(dom, name) {
return dom.querySelector(`meta[name='${name}']`)?.attributes?.content?.value;
}
function getSrcById(dom, id) {
return dom.querySelector("#" + id)?.attributes?.src?.value;
}
function getFaviconUrl(dom, url) {
const iconHref =
dom.querySelector("link[rel='icon']")?.attributes?.href?.value ??
dom.querySelector("link[rel='shortcut icon']")?.attributes?.href?.value;
if (!iconHref) {
return new URL(url).origin + "/favicon.ico";
}
if (iconHref.includes("http")) {
return iconHref;
}
if (iconHref.startsWith("//")) {
return new URL(url).protocol + iconHref;
}
const baseUrl = dom.querySelector("base")?.attributes?.href?.value;
if (baseUrl) {
return baseUrl + iconHref;
}
return new URL(url).origin + iconHref;
}
function isSecure(url) {
return url && !url.startsWith("http://");
}
async function createCard(url, descMaxLen) {
const html = await fetchAsDom(url);
const siteName = getMetaByProperty(html, "og:site_name") ?? new URL(url).host;
const title =
getMetaByProperty(html, "og:title") ??
getMetaByProperty(html, "title") ??
html.querySelector("title")?.text ??
url;
const description =
getMetaByProperty(html, "og:description") ??
getMetaByProperty(html, "description") ??
getMetaByName(html, "description") ??
"";
const faviconUrl = getFaviconUrl(html, url);
const imageUrl =
getMetaByProperty(html, "og:image") ??
getSrcById(html, "ebooksImgBlkFront");
const imageDom = isSecure(imageUrl)
? `<img src="${imageUrl}" class="link-card-image"/>`
: "";
return `<div class="link-card">
<div class="link-card-header">
<img src="${faviconUrl}" class="link-card-site-icon"/>
<span class="link-card-site-name">${siteName}</span>
</div>
<div class="link-card-body">
<div class="link-card-content">
<div>
<p class="link-card-title">${title}</p>
</div>
<div class="link-card-description">
${trimMax(description, descMaxLen)}
</div>
</div>
${imageDom}
</div>
<a href="${url}"></a>
</div>`;
}
module.exports = createCard;
```
見ての通り[[XSS]]に耐性のないコードです。しかし、ブラウザ参照時に自動でコードが実行されるわけではないため、大事には至らないでしょう。万が一、不正な文字列が埋め込まれていた場合は `publish` 前に直接取り除けばいい話です。[[Obsidian]]も[[Obsidian Publish]]も[[CORS]]制限には厳しいので問題にはならないはずです。
## カードでデザイン
[[CSS]]も1から独自にデザインしました。カスタマイズ性が高いのは自分で作成することのメリットであり、醍醐味の1つです。その分、大変なことも多く時間もかかりますが..。
```css:main.css
/************************/
/* card link
/************************/
/* Mobile */
@media screen and (max-width: 1024px) {
.link-card {
margin: 10px;
padding: 25px 15px;
position: relative;
z-index: 1;
background-color: rgba(100, 100, 100, 0.3);
}
.link-card-image {
width: 100% !important;
max-height: 200px !important;
object-fit: contain !important;
margin: 10px 0 0 !important;
}
}
/* PC */
@media screen and (min-width: 1024px) {
.link-card {
margin: 40px 15px;
padding: 15px 30px;
position: relative;
z-index: 1;
background-color: rgba(100, 100, 100, 0.3);
}
.link-card-body {
display: flex;
justify-content: space-between;
}
.link-card-content {
padding-right: 50px;
}
.link-card-image {
display: inline-block;
vertical-align: middle;
height: 100px !important;
margin: 0 !important;
object-fit: contain;
}
}
.link-card a {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-indent: -999px;
z-index: 2;
}
.link-card:hover {
cursor: pointer;
backdrop-filter: brightness(150%);
transition: backdrop-filter 0.5s;
}
.link-card-header {
display: flex;
align-items: center;
}
.link-card-title {
font-weight: bolder;
font-size: 110%;
word-break: break-all;
}
.link-card-description {
color: grey;
font-size: 90%;
}
.link-card-site-icon {
object-fit: contain !important;
margin: 0 5px 0 0 !important;
height: 15px !important;
}
.link-card-site-name {
color: lightgray;
font-size: 90%;
}
```
PCのように横幅が広い画面と、モバイルのように横幅が狭い画面では異なるレイアウトにしています。レイアウトには若干迷いがあり、今後大幅に変更する可能性があります。[[HTML]]の修正が必要になると修正範囲が広がるため、なるべく[[CSS]]で済ませたいと思っています。
## 自分の[[Vault]]でも使えるように
ここまでの内容を見て、「自分の[[Vault]]でも是非使ってみたい」と思っていただけた方のために、設定の仕方を紹介します。とは言っても[[🧊Obsidian Templater Insert link card]]の `README.md` を読んでもらうだけです。
<div class="link-card">
<div class="link-card-header">
<img src="https://github.githubassets.com/favicons/favicon.svg" class="link-card-site-icon"/>
<span class="link-card-site-name">GitHub</span>
</div>
<div class="link-card-body">
<div class="link-card-content">
<div>
<p class="link-card-title">GitHub - tadashi-aikawa/obsidian-templater-insert-link-card: Snippets that enable inserting a link as a card using Templater Obsidian plugin</p>
</div>
</div>
<img src="https://opengraph.githubassets.com/c9bb309e1408fe18a9bc8477bfe129303947b5258d76b0fd2d4b02a8b27a3128/tadashi-aikawa/obsidian-templater-insert-link-card" class="link-card-image"/>
</div>
<a href="https://github.com/tadashi-aikawa/obsidian-templater-insert-link-card#get-started"></a>
</div>
`main.js` や `main.css` の内容は今後も変更されることが予想されるためリポジトリを作りました。将来、本記事の内容は最新のリポジトリより古くなると思います。
また、[[Templater]]やテーマの設定をしたことない人にはハードルが高く感じるかもしれません。そのような方は[[Rich Links]]の公開を待った方がいいでしょう。以下のプルリクエストが通れば使えるようになるはずです。
<div class="link-card">
<div class="link-card-header">
<img src="https://github.githubassets.com/favicons/favicon.svg" class="link-card-site-icon"/>
<span class="link-card-site-name">GitHub</span>
</div>
<div class="link-card-body">
<div class="link-card-content">
<div>
<p class="link-card-title">Add obsidian-rich-links by dhamaniasad · Pull Request #378 · obsidianmd/obsidian-releases</p>
</div>
</div>
<img src="https://opengraph.githubassets.com/44db028952e86e16315e4d6627c126e95f5a935716f2e3fd8026a2f25680aaeb/obsidianmd/obsidian-releases/pull/378" class="link-card-image"/>
</div>
<a href="https://github.com/obsidianmd/obsidian-releases/pull/378"></a>
</div>
実装を見る限り、[[iframely]]の利用有無以外で大きな違いはなさそうです。デザインや処理のカスタマイズ性を求めないのであれば十分でしょう。
## まとめ
[[Templater]]を使って、[[Obsidian]]や[[Obsidian Publish]]にてカード形式のリンクを作成できるようにする方法を紹介しました。
[[URL]]を指定するだけでよしなに描画してくれる機能に比べれば、仕様変更時の修正も大変で泥臭いかもしれません。その反面、[[REST API]]による構造の取得をスキップできるので、描画速度の面では優れていると思います。
==[[Obsidian]]は本来『第2の脳』になるべき場所であり、必要以上に見た目を気にかけるべきではない==と思っています。しかし、[[📒Articles]]は例外的に閲覧者を強く意識したコンテンツになっていますので、今回のような対応もアリかなと思っています。
[^1]: 難読化や[[Minification]]されたソースコードを[[Notes/publish.js]]や[[Obsidianプラグイン]]に埋め込めば実現できるかもしれないが、アカウント登録が必要な[[embed.ly]]に対するそれはライセンス的にNGだろう
[^2]: [[iframely]]の[[REST API]]を使えば解消しそう..だが、アカウント登録しなければ、公式サイトに公開されていないため不特定多数への公開はグレーだと思う
[^3]: 個人の利用に限って言えば、自己責任で[[iframely]]の[[REST API]]を使ったソリューションに寄せる手もある