生産性のない時間 is プライスレス

Astroで作ったブログにタグ機能を追加した。

公開日時:

Astroで実装している個人ブログにタグ機能を追加していこうと思います。

具体的には以下のURLパスのページが生成されるようにします。

実装

タグをコレクションのスキーマに定義する

このブログではAstroのContent collectionsを利用して記事を管理しています。

なので、まずはタグを管理できるように、content.config.tsにタグのスキーマを定義します。

import { defineCollection, z } from "astro:content";

const posts = defineCollection({
	schema: z.object({
		title: z.string(),
		description: z.string(),
		pubDate: z.date(),
		updatedDate: z.date().optional(),
		draft: z.boolean(),
		tags: z.array(z.string()).optional(), // タグを追加
	}),
});

export const collections = {
	posts,
};

タグ一覧ページの作成

次にタグ一覧ページの作成です。

tags/index.astroファイルを作成します。

---
import { getCollection } from "astro:content";
import Layout from "../../layouts/Layout.astro";

const posts = await getCollection("posts");
const tagSet = new Set();
for (const post of posts) {
  if (Array.isArray(post.data.tags)) {
    for (const tag of post.data.tags) {
      tagSet.add(tag);
    }
  }
}
---

<Layout
  title="タグ一覧"
  description="このブログで使われている全タグの一覧です。"
  slug="tags"
>
  <h1>タグ一覧</h1>
  <ul class="tag-list">
    {
      (async () => {
        return Array.from(tagSet)
          .sort()
          .map((tag) => (
            <li class="tag-item">
              <a
                href={`/tags/${encodeURIComponent(String(tag))}`}
                class="tag-link"
              >
                {tag}
              </a>
            </li>
          ));
      })()
    }
  </ul>
</Layout>
  1. getCollection関数で全件の記事を取得する。
  2. tagSetを用意し、その中に記事から取得したtagを逐次追加していく。

Setは重複した値は入らずに一意になる性質を持っているのでフィルタリング処理的なものを書かなくてもこれで正常に動作します。

タグ別記事一覧の作成

タグ別の記事一覧を作成します。

/tags/[tag]と動的ルートになっているので、getStaticPaths関数を利用して実際に生成するページを定義する必要があります。 詳しくはルーティング | Docsに書いてあります。

---
import Layout from "../../layouts/Layout.astro";
import { getCollection } from "astro:content";

export async function getStaticPaths() {
    const allPosts = await getCollection("posts");
    const tagSet = new Set<string>();
    for (const post of allPosts) {
        if (Array.isArray(post.data.tags)) {
            for (const tag of post.data.tags) {
                tagSet.add(tag);
            }
        }
    }
    return Array.from(tagSet).map((tag) => ({ params: { tag } }));
}
const { tag } = Astro.params;
if (!tag) {
    throw new Error("Tag parameter is required");
}
const allPosts = await getCollection("posts");
const posts = allPosts
    .filter(({ data }) => {
        return (
            data.draft !== true &&
            Array.isArray(data.tags) &&
            data.tags.includes(tag)
        );
    })
    .sort((a, b) => {
        return (
            new Date(b.data.pubDate).getTime() -
            new Date(a.data.pubDate).getTime()
        );
    });
---

<Layout
    title={`タグ: ${tag}`}
    description={`「${tag}」タグが付いた記事一覧です。`}
    slug={`tags-${tag}`}
>
    <ul>
        {
            posts.map((post) => (
                <li>
                    <time datetime={post.data.pubDate as unknown as string}>
                        {new Date(post.data.pubDate).toLocaleDateString(
                            "ja-JP",
                            {
                                year: "numeric",
                                month: "short",
                                day: "numeric",
                            },
                        )}
                    </time>
                    <a href={`/${post.slug}`}>{post.data.title}</a>
                </li>
            ))
        }
    </ul>
</Layout>
  1. コレクションから全記事を取得する。
  2. tagSetを用意しその中に記事から取得したtagを逐次追加していく。

と先ほどと同じ手段で実現できますが、一点だけ注意事項です。

多分動作原理が分かっていれば自明なのだと思いますが、getCollectionを以下のように使いまわそうとすると実行時エラーになります。

const allPosts = await getCollection("posts");

export async function getStaticPaths() {
    const tagSet = new Set<string>();
    for (const post of allPosts) {
        if (Array.isArray(post.data.tags)) {
            for (const tag of post.data.tags) {
                tagSet.add(tag);
            }
        }
    }
    return Array.from(tagSet).map((tag) => ({ params: { tag } }));
}
const { tag } = Astro.params;
if (!tag) {
    throw new Error("Tag parameter is required");
}
const posts = allPosts
    .filter(({ data }) => {
        return (
            data.draft !== true &&
            Array.isArray(data.tags) &&
            data.tags.includes(tag)
        );
    })
    .sort((a, b) => {
        return (
            new Date(b.data.pubDate).getTime() -
            new Date(a.data.pubDate).getTime()
        );
    });

小手先のパフォーマンス改善的なことをしたくなる気持ちはとても良く分かる(実際私はした)のですが、 おとなしく愚直に実装しましょう……。

記事にタグを表示する

こちらは一番わかりやすいやつです。 記事ページにこんな感じで実装すれば終了します。

      {
        Array.isArray(post.data.tags) && post.data.tags.length > 0 && (
          <div class="tags">
            <span>タグ: </span>
            {post.data.tags.map((tag) => (
              <a href={`/tags/${encodeURIComponent(tag)}`} class="tag">
                {tag}
              </a>
            ))}
          </div>
        )
      }

まとめ

というわけでざっくりタグ機能を実装しました。 まぁほぼ引っかかるところはなかったのですが、getStaticPathsの挙動は少し気になったというか、 Astroテンプレートの処理の順番だとは思うのでそこは少し追ってみたくなったかもしれません。

そして今回これに伴って全記事に対してCopilot先生にタグをつけてもらったのですが、なんかやっぱ思想的な奴が多い気がする……。 これはzennには書けないなぁと思いました(小並感)

今後してみたいこと