Astroでもバックリンクしたい!
Aizu Advent Calendar 2023 5日目 AstroのContent Collectionsを解析することで、バックリンクを生成する。

Astroでもバックリンクしたい!

はじめに

Aizu Advent Calendar 2023について

この記事はAizu Advent Calendar 2023の5日目だ。会津大のメンバーが投稿しているので是非みてほしい。

Aizu Advent Calendar

バックリンクとは

バックリンクとは被リンクの一覧のことだ。 あるページにおいて、自身がどのページがらリンクされているかを表示することで、 ページ間の移動を簡単にするという目的で設置されることが多い(あとはSEO的に強いとかなんとか)

記事がデータベースに保存してあるようなプロジェクトで作るのが一般的だと思う。

qiitaでの例

実装

概要

バックリンクをこのブログに実装した。Astroの機能を利用しているのでAstro専用である。

各ページでContent Collectionsをすべて解析することで、SSGに対応しつつdev serverにも対応した。

全体としては、markdownの解析を行ないフロントマターに順方向のリンク一覧を書き込むRehypeプラグインと、 astroコンポーネント内ですべてのmarkdownのフロントマターを読み、逆方向のリンクに変換する関数で構成される。

どうしてそのような設計になったか。あるいは設計の苦悩

今回、このブログに実装する前に、3種類の設計を試した。

  • viteのプラグインを作成。Astroのビルドがおわった後に生成されたhtmlを解析し、リンク関係をjsonに書きだす。クライアントでそのjsonをfetch。
  • 各ページでmarkdownをレンダリングし、htmlを解析する。

そして採用した、

  • rehypeプラグインを作成。markdownのレンダリング時にリンクを一覧し、frontmatterに書き込む。各ページでバックリンクに変換。

それぞれに利点と欠点がある。それぞれを箇条書きする。

  • viteのプラグインを作成する方法

    • メリット

      • ビルド後のhtmlを見るので、Content Collectionsに限らない。

        • micro CMSをコンテンツの管理に使うとか、 MDXでコンポーネントを使うとか。
        • ssgするフレームワークなら、astro以外でも利用できる。
      • 実装が簡単。
      • 依存が少ない。
    • デメリット

      • ssgできない。
      • astroコンポーネントが使えない(致命的)

        • astroコンポーネントはクライアントで利用できないので。
  • 各ページでhtmlを解析する方法

    • メリット

      • mdxに対応できる。
    • デメリット

      • 実装がダルい。

        • Content Collectionsをhtmlにする手段が限られすぎている。 というのも、唯一存在するAPIがコンポーネントのslotに生えているrender関数しかない。
      • astroコンポーネントが使えない(致命的)

        • slotのrender関数を利用する関係で、データの取り回しが面倒すぎる。
      • astro専用
  • rehypeプラグインを作成する方法

    • メリット

      • 実装がシンプル。
      • 依存が少ない。
    • デメリット

      • astro専用
      • mdxに対応できない。

最終的に3番目の方法を選択したのはご存知のとおりである。

実装のくふう

次のコードはrehypeのプラグインの一部。ここでhastを読み、フロントマターに書き込んでいる。

    const forlinkNodes: hast.Element[] = [];
    visit(tree, (node, _index, _parent) => {
        if (node.type === 'element' && node.tagName === 'a')
            forlinkNodes.push(node);
    });

    setVfileFrontmatter(file, {
        forlink: forlinkNodes
            .map(node =>
                node.properties == null ?
                    {} :
                    node.properties
            )
            .map(properties => properties.href)
            .filter((link): link is string => typeof link == 'string'),
    });

そしてgetStaticPaths()の中でフロントマターを読み、各ページでレンダリングする。 astro devを実行中、content collectionsの更新にあわせてバックリンクを再生成することができる。

export async function getStaticPaths() {
    const entries = await getCollection("blog");

    const backlinks = await getBacklinks([entries]);

    return entries.map((entry) => ({
        params: { slug: entry.slug },
        props: {
            entry,
            backlink:
                backlinks.get(`/${entry.collection}/${entry.slug}/`) ?? [],
        },
    }));
}

おわりに

実装について

Astroに苦しめられた感が強い。

アドカレについて

アドカレに衝動的に登録したが、ギリギリ間に合った。 もうすこし余裕をもって登録すべき。

Backlinks