記事「Astroブログで記事の変更履歴を載せる」のサムネイル

Astroブログで記事の変更履歴を載せる

記事ごとの更新履歴を自動で取得し、ページに掲載する仕組みを作った。git logコマンドを活用し、いつどんな変更をしたかを自動で出力する。

背景

かねてから、記事の変更履歴をどのように掲載するかを考えていた。技術記事は内容を更新することが多いため、「どこがいつ変わったか」を公開すれば、誤解を減らし、信頼性を高められる。手動で書くのは忘れそうなため、Gitのログの情報を使うことにした。

このブログのコード全体は以下のリポジトリから閲覧できる。

GitHub - pullriku/pullriku-blog-astro: 自分用のブログ
自分用のブログ. Contribute to pullriku/pullriku-blog-astro development by creating an account on GitHub.
GitHub - pullriku/pullriku-blog-astro: 自分用のブログ favicon github.com
GitHub - pullriku/pullriku-blog-astro: 自分用のブログ

ChatGPTに相談しながら実施した。

ChatGPT - Git log 特定ファイル
Shared via ChatGPT
ChatGPT - Git log 特定ファイル favicon chatgpt.com
ChatGPT - Git log 特定ファイル
環境
  • Node.js 24.3.0
  • Astro 5.7.12
  • Git 2.50.1

git logで履歴を取得する

git log -- ファイル名で指定したファイルに関する変更履歴を取得できる。これに加え、JSON形式で取得するように--pretty=format:を指定する。

Git - git-log Documentation
Git - git-log Documentation favicon git-scm.com
git log --pretty=format:'{"hash":"%H","author":"%an","date":"%ad","subject":"%s"},' -- ファイル名

ビルド時にこのコマンドを実行し、得た情報を記事の最後に記載する。

実装

ファイル名を引数に取り、ログを返す関数を作る。Node.jsのexecSyncでコマンドを実行できる。

Child process | Node.js v24.4.0 Documentation
Child process | Node.js v24.4.0 Documentation favicon nodejs.org
lib/gitlog.ts
import { execSync } from "node:child_process";

export type GitLog = {
    hash: string;
    author: string;
    date: string;
    subject: string;
};

export function gitLog(filepath: string): GitLog[] {
    const output = execSync(
        `git log --pretty=format:'{"hash":"%H","author":"%an","date":"%ad","subject":"%s"},' -- ${filepath}`,
    ).toString();

    const json = `[${output.trim().replace(/},$/, "}")}]`;

    return JSON.parse(json);
}

これだけだと記事ごとにファイルパスを毎回指定する必要があって不便。そこで、記事IDからパスを解決し、履歴を加工して返すラッパー関数を作る。加工は以下の処理を行っている。

  • 日付を日付型に変換する
  • コミット名の:以降を取り出す
  • Mergeは除外する
  • 「公開」の前の履歴を消す

また、マークダウンとMDXのどちらも受け付けられるように、isMdxを引数に取るようにした。

lib/gitlog.ts(追加)
import dayjs from "dayjs";

export type PostHistory = {
    date: dayjs.Dayjs;
    subject: string;
};

export function getPostHistory(id: string, isMdx: boolean): PostHistory[] {
    const path = `src/contents/posts/${id}.${isMdx ? "mdx" : "md"}`;

    const iter = gitLog(path)
        [Symbol.iterator]()
        .map((log) => {
            const subject = log.subject;
            const index = subject.indexOf(":");
            return {
                date: dayjs(log.date),
                subject:
                    index !== -1
                        ? subject.slice(index + 1).trim()
                        : subject.trim(),
            };
        })
        .filter((log) => !log.subject.startsWith("Merge"));

    const sorted =  Array.from(iter).reverse();

    const pubIndex = sorted.findIndex((log) => log.subject.startsWith("公開"));
    let result: PostHistory[];
    if (pubIndex) {
        result = sorted.slice(pubIndex);
    } else {
        result = sorted;
    }

    return result;
}

これで記事の変更履歴を取得できるようになった。変更履歴を表すコンポーネントを作り、この関数を呼び出すようにする。

components/PostHistory.astro
---
import type { CollectionEntry } from "astro:content";
import { type PostHistory, getPostHistory } from "@lib/gitlog";
import dayjs from "dayjs";
import timezone from 'dayjs/plugin/timezone';
import utc from 'dayjs/plugin/utc';

dayjs.extend(utc);
dayjs.extend(timezone);

type Props = {
    post: CollectionEntry<"posts">;
};

const { post } = Astro.props;
const isMdx = (post.filePath ?? "").endsWith(".mdx");

const logs: PostHistory[] = Array.from(getPostHistory(post.id, isMdx));
---
{
    logs.length !== 0 && (
        <h2>変更履歴</h2>
        <table>
            <tr>
                <th>日時</th>
                <th>内容</th>
            </tr>
            {
                logs.map((log) => (
                    <tr>
                        <td>
                            <time datetime={log.date.toString()}>
                                {log.date.tz("Asia/Tokyo").format("YYYY-MM-DD HH:mm:ss Z")}
                            </time>
                        </td>
                        <td>{log.subject}</td>
                    </tr>
                ))
            }
        </table>
    )
}

このPostHistoryコンポーネントを記事のページ(私の場合はpages/posts/[id].astro)から呼び出せば完成だ。

まとめ

ビルド時にgit logで記事ごとの履歴を取って、Astroコンポーネントで表示できるようにした。

変更履歴

日時内容
公開
参考リンクを追加