ブログ記事にタグを追加し、タグごとの記事一覧を作る


DE-TEIUです。ブログ記事にタグを付けたくなりました。やっていきます。

ブログ記事にタグを追加する

content.config.ts に1行追加。 こうすると、各記事のmd、mdxファイルの上部でtagsのパラメータを定義できる。

const blog = defineCollection({
  // Load Markdown and MDX files in the `src/content/blog/` directory.
  loader: glob({ base: "./src/content/blog", pattern: "**/*.{md,mdx}" }),
  // Type-check frontmatter using a schema
  schema: z.object({
    title: z.string(),
    description: z.string(),
    // Transform string to Date object
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    heroImage: z.string().optional(),
    tags: z.array(z.string()).optional(), // これを追加する
  }),
});

これで記事の上部にtagsを追加できるようになる。

---
title: "記事タイトル"
description: "記事の説明"
pubDate: "2025-01-05"
heroImage: ""
tags: ["IT","Astro"]
---

記事ページの上部にタグを表示してみる

ブログで使用するタグを適当な定数定義用のファイルとかにまとめておく。例えばこう。

export const TAGS = [
  {
    tag: "it",
    name: "IT",
  },
  {
    tag: "book",
    name: "本",
  },
  {
    tag: "kusoapp",
    name: "クソアプリ",
  },
];

ついでにタグ名からタグを取り出すメソッドも作っておく。

/**
 * タグ名からタグ情報を取得する
 * @param name タグ名
 * @returns タグ情報
 */
export const getTagFromName = (name: string) => {
  return TAGS.find((tag) => tag.name === name);
};

あとはブログ記事ページの上部でリンク付きのタグ一覧を表示するように改修。

const { title, description, pubDate, updatedDate, heroImage, tags } =
  Astro.props;
<div>
  <ul class="flex flex-row justify-center items-center space-x-2">
    {
      tags!.map((tag) => (
        <li class="list-none">
          <a
            href={`/tags/${getTagFromName(tag)!.tag}/1`}
            class="rounded-md bg-teal-600 py-1 px-3 border border-transparent text-base text-white transition-all shadow-sm hover:bg-teal-700 hover:text-white hover:shadow-md"
          >
            {tag}
          </a>
        </li>
      ))
    }
  </ul>
</div>

これでタグの追加ができるようになった。 タグをクリックすると、タグごとの記事一覧画面(/tags/[タグ]/1)に遷移する。(末尾の1はページ番号)

タグごとの記事一覧画面を作る

次はタグごとの記事一覧画面を作っていこう。

/tags/[tag]/ というパスでディレクトリを作成。そこに[page].astroを作る。 中身はこんな感じ。

---
import { type CollectionEntry, getCollection } from "astro:content";
import ArticleList from "../../../components/ArticleList.astro";
import { TAGS, PAGE_ARTICLE_UNIT } from "../../../consts";

interface Params {
  tag: string;
  page: number;
}
interface Props {
  posts: CollectionEntry<"blog">[];
  pageCount: number;
}

export async function getStaticPaths() {
  const posts = (await getCollection("blog")).sort(
    (a: CollectionEntry<"blog">, b: CollectionEntry<"blog">) =>
      b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
  );
  const items: any = [];

  // タグごとに記事を取り出す
  TAGS.forEach((tag) => {
    const tagPosts = posts.filter((post) => post.data.tags!.includes(tag.name));
    items.push({
      tag: tag,
      posts: tagPosts,
    });
  });

  // 記事をページ単位で分割
  const results: any = [];
  items.forEach((item: any) => {
    const posts = item.posts;
    const tag = item.tag;
    const pageCount = Math.ceil(posts.length / PAGE_ARTICLE_UNIT);
    for (let i = 1; i <= pageCount; i++) {
      results.push({
        params: {
          tag: tag.tag,
          page: i,
        },
        props: {
          posts: posts.slice(
            (i - 1) * PAGE_ARTICLE_UNIT,
            i * PAGE_ARTICLE_UNIT
          ),
          pageCount,
        },
      });
    }
  });
  return results;
}

const posts = Object.values(Astro.props.posts) as CollectionEntry<"blog">[];
const pageCount = Astro.props.pageCount as number;
const { tag, page } = Astro.params as Params;
const hasNext = pageCount > page;
const hasPrev = page > 1;
const nextURL = hasNext ? `/tags/${tag}/${Number(page) + 1}/` : "";
const prevURL = hasPrev ? `/tags/${tag}/${Number(page) - 1}/` : "";
---

<ArticleList posts={posts} nextURL={nextURL} prevURL={prevURL} />

あとは記事一覧(ArticleList.astro)のコンポーネントに「前のページ」「次のページ」のリンクを置いていく。

interface Props {
  posts: CollectionEntry<"blog">[];
  nextURL: string;
  prevURL: string;
}
~~~

<nav class="flex justify-between mt-8">
  <a href={prevURL}> {!prevURL ? "" : "前のページ"} </a>
  <a href={nextURL}> {!nextURL ? "" : "次のページ"} </a>
</nav>

ということで完成。