AstroのContent CollectionsでMarkdown内のリンクをいい感じに表示する方法
やりたいこと
自分のサイトでは、ブログのコンテンツ管理に、Astro.jsのContent Collectionsという機能を使っています。
デフォルトでは、リンクをMarkdownに書いたときに、リンクカードが表示されません。
↓ こういうやつ

これをいい感じに表示できる方法を見つけたので残しておきます。
unified.jsについて
Astoでは、Markdownをhtmlに変換するために、unified.jsというユーティリティが使われています。
これは、
- Markdownをパースして木構造(mdast)に変換
 - Markdownの構造を編集
 - Markdownの木構造(mdast)からhtmlの木構造(hast)に変換
 - htmlの構造を編集
 - html文字列に変換
 
という一連の流れを、それぞれの工程ごとにモジュール化して実行できるようにするエコシステムです。
特に、Markdownを扱う部分とhtmlを扱う部分をそれぞれremark, rehypeというツールが担っています。(それぞれ単体で利用することもできます。)
Astroでは、内部的にこの仕組みを利用していて、先程のリストに示した2, 4の工程で使うモジュール(プラグイン)を設定ファイル内で定義することができます。
使用するプラグイン
remark側
リンクカードを実現するために使用するプラグインを紹介します。
remark側では、remark-link-cardというプラグインを使用します。
このプラグインは、Markdownからリンクを抜きだし、そこにリンクカードに必要な情報(タイトルや説明、画像など)をwebから取得して追加する機能を提供してくれます。
以下、READMEから抜粋して引用
# remark-link-card
## Bare links
Bare links are converted to link cards.
http://example.com/
https://www.npmjs.com/package/remark-link-card
<http://example.com/>
<https://www.npmjs.com/package/remark-link-card>
## Inline links
Inline links are **not** converted to link cards.
[example](http://example.com/) is inline link
[remark-link-card](https://www.npmjs.com/package/remark-link-card) is inline link
## Multiple links in one line
If there are multiple links in one line, they will **not** be converted to link cards.
http://example.com/ http://example.com/ http://example.com/
上のように入力すると、以下のように出力されます。
# remark-link-card
## Bare links
Bare links are converted to link cards.
<a class="rlc-container" href="http://example.com/">
  <div class="rlc-info">
    <div class="rlc-title">Example Domain</div>
    <div class="rlc-url-container">
      <img class="rlc-favicon" src="https://www.google.com/s2/favicons?domain=example.com" alt="Example Domain favicon" width="16px" height="16px">
      <span class="rlc-url">http://example.com/</span>
    </div>
  </div>
</a>
<a class="rlc-container" href="https://www.npmjs.com/package/remark-link-card">
  <div class="rlc-info">
    <div class="rlc-title">remark-link-card</div>
    <div class="rlc-description">remark plugin to convert literal link to link card</div>
    <div class="rlc-url-container">
      <img class="rlc-favicon" src="https://www.google.com/s2/favicons?domain=www.npmjs.com" alt="remark-link-card favicon" width="16px" height="16px">
      <span class="rlc-url">https://www.npmjs.com/package/remark-link-card</span>
    </div>
  </div>
  <div class="rlc-image-container">
    <img class="rlc-image" src="https://static.npmjs.com/338e4905a2684ca96e08c7780fc68412.png" alt="remark-link-card" width="100%" height="100%"/>
  </div>
</a>
<a class="rlc-container" href="http://example.com/">
  <div class="rlc-info">
    <div class="rlc-title">Example Domain</div>
    <div class="rlc-url-container">
      <img class="rlc-favicon" src="https://www.google.com/s2/favicons?domain=example.com" alt="Example Domain favicon" width="16px" height="16px">
      <span class="rlc-url">http://example.com/</span>
    </div>
  </div>
</a>
<a class="rlc-container" href="https://www.npmjs.com/package/remark-link-card">
  <div class="rlc-info">
    <div class="rlc-title">remark-link-card</div>
    <div class="rlc-description">remark plugin to convert literal link to link card</div>
    <div class="rlc-url-container">
      <img class="rlc-favicon" src="https://www.google.com/s2/favicons?domain=www.npmjs.com" alt="remark-link-card favicon" width="16px" height="16px">
      <span class="rlc-url">https://www.npmjs.com/package/remark-link-card</span>
    </div>
  </div>
  <div class="rlc-image-container">
    <img class="rlc-image" src="https://static.npmjs.com/338e4905a2684ca96e08c7780fc68412.png" alt="remark-link-card" width="100%" height="100%"/>
  </div>
</a>
## Inline links
Inline links are **not** converted to link cards.
[example](http://example.com/) is inline link
[remark-link-card](https://www.npmjs.com/package/remark-link-card) is inline link
## Multiple links in one line
If there are multiple links in one line, they will **not** be converted to link cards.
http://example.com/ http://example.com/ http://example.com/
直接リンクを段落に入れた場合のみ、リンクカードの構造が適用されます。
このプラグインはあくまで構造のみを提供してくれるため、class名を使って自分でスタイルを当てることができます。
これにより、自分のサイトに在ったデザインでリンクカードを設置できるところが魅力的です。
rehype側
上記のプラグインだけでリンクカードを設置することはできますが、外部サイトへのリンクの場合は新しいタブで開くようにするほうが親切です。(この問題はremark-link-cardのissueでも指摘されています。)
外部リンクの場合のみ自動的にaタグに属性を追加するために、2つのプラグインをrehype側で使用します。
まず、remark-link-cardの出力を見ると、リンクカードはMarkdownの中に生のhtmlとして埋めこまれています。
このままだと、mdastからhastに変換したときに生のhtml文字列として認識されてしまい、aタグで囲まれた構造として認識されず、後述の属性を自動的に追加するプラグインに無視されてしまいます。
そこで、先にrehype-rawというプラグインを使用して再度htmlをパースし直します。
次に、rehype-external-linksというプラグインを使用して、外部を向いたリンクのaタグに自動的にtarget="_blank"などの属性を追加します。
ここはこの順番でプラグインが適用されるように設定を書く必要があります。
Astroの設定
remark, rehypeのプラグインは、astro.config.js内で定義できます。
まずは、プラグインのパッケージをプロジェクトに追加します。
npm install remark-link-card rehype-raw rehype-external-links
次に、設定ファイルに以下のように記述します。
import { defineConfig } from 'astro/config'
import rlc from 'remark-link-card'
import rehypeRaw from 'rehype-raw'
import rehypeExternalLinks from 'rehype-external-links'
export default defineConfig({
  markdown: {
    remarkPlugins: [
      [
        rlc,
        { shortenUrl: true },
      ],
    ],
    rehypePlugins: [
      rehypeRaw,
      [
        rehypeExternalLinks,
        { target: '_blank' },
      ],
    ],
  },
});
このように設定すると、リンクカードを実現することができます。
以上です。
Discussion