この文書では、マークダウンベースのブログでフロントマターをGitベースで自動管理する方法を扱います。
開発ドキュメントやブログ投稿を管理していると、誰もが経験する問題があります。内容を修正してコミットしたものの、肝心なドキュメントのメタデータは更新し忘れるというミスです。Gitヒストリーには正確な修正時刻が記録されているのに、フロントマターの日付は依然として過去のままです。
# 実際のコミットは2025-01-13なのに...
dates:
- '2025-01-01T10:00:00+09:00' # 更新し忘れ
これは単純なミスを超えて、開発プロセスの効率性問題につながります。Gitがすでにすべての変更履歴を追跡しているにもかかわらず、私たちは同じ情報を手動で重複管理しているのです。これはDRY(Don't Repeat Yourself)原則に違反し、Single Source of Truthの概念にも反します。
問題解決のための最初のアプローチは、Git hooksを活用することでした。コミット時点で自動的にフロントマターを更新する簡単なスクリプトを作成すれば良いと予想していました。
しかし、実際の実装過程で予想外の技術的な難関が連鎖的に発生しました:
各問題は単純に見えましたが、解決過程でGitの内部動作と様々なツールの詳細を深く理解する必要がありました。
Git hooksは、Gitの特定イベントが発生したときに自動的に実行されるスクリプトです。最初に試したのはpre-commit hookでした。
# .git/hooks/pre-commit
#!/bin/sh
echo "フロントマター更新中..."
node update-frontmatter.js
実行時点をテストした結果、pre-commit hookはコミットオブジェクトが生成される前に実行されるため、コミットメッセージやハッシュにアクセスできないことを確認しました。これはGitのコミットプロセスを理解する重要なきっかけとなりました。
Gitのコミットプロセスは次の順序で進行します:
このような実行時点の違いを正確に理解せずに実装を始めたのが最初のミスでした。技術文書の詳細を十分に検討せず、直感だけに頼った結果でした。
Post-commit hookに切り替えた後、コミット情報にアクセスできるようになりました。しかし、新たな問題が発生しました。
git log --oneline
f3d2a1b フロントマター自動更新
a8c9e2f 🚀 新機能追加
7b5c3d2 フロントマター自動更新
9d1e4f6 🐛 バグ修正
フロントマターを更新して新しいコミットを生成すると、ヒストリーが不必要に複雑になる問題がありました。各コミットごとにメタデータ更新のための追加コミットが生成され、プロジェクトヒストリーの可読性を損なうことになります。
これを解決するためにgit commit --amend
コマンドを活用することにしました。このコマンドは最新のコミットを修正して、新しいコミットを生成せずに変更内容を反映できるようにします。
# Post-commit hook内部
git add -u
git commit --amend --no-edit --no-verify
このアプローチを通じて、クリーンなコミットヒストリーを維持しながらメタデータを自動的に更新できました。
.git/hooks
ディレクトリは、Gitが追跡しないローカル設定です。これはセキュリティ上の理由で設計されたものですが、チーム全体が同じhooksを使用する必要がある状況では制約事項となります。
各チームメンバーが手動でhooksファイルをコピーする必要があるなら、自動化の意味が薄れます。また、hooksが更新されるたびに全チームメンバーが同期する必要がある管理負担も発生します。
Huskyは、Git hooksをプロジェクトの一部として管理できるようにするツールです。2010年代半ばに登場したこのツールは、JavaScriptプロジェクトでGit hooksをnpmパッケージのように管理できるようにします。
npm install --save-dev husky
npx husky install
npx husky add .husky/post-commit "node scripts/update-frontmatter.js"
Huskyを通じて、hooksはプロジェクトリポジトリの一部となり、npm install
時に自動的に設定されます。これにより、チーム全体が一貫した開発環境を維持できるようになりました。
{
"scripts": {
"prepare": "husky install"
}
}
この設定をpackage.json
に追加すれば、プロジェクトをクローンした後npm install
だけでHuskyが自動的に設定されます。
すべての設定が完了したと判断してテストを進めました。しかし、予想外の状況が発生しました。
post-commit hookが無限に繰り返し実行される現象が発生しました。分析の結果、次のような循環構造が形成されていました:
git commit --amend
実行これはGit hooksの再帰的実行を制御するメカニズムがないことを示す事例でした。
最初の試みは--no-verify
フラグを使用することでした。このフラグはGit hooksを回避するオプションとして知られています。
しかし、Git文書を詳しく確認した結果、--no-verify
フラグはpre-commitとcommit-msg hooksのみを回避し、post-commit hookには影響を与えないことを発見しました。これは設計上、post-commitはすでにコミットが完了した後の通知目的で使用されるためです。
様々な解決策を検討した末、環境変数を活用したシンプルかつ効果的な方法を実装しました。
#!/bin/sh
# すでに実行中なら終了
if [ "$FRONTMATTER_UPDATE_RUNNING" = "1" ]; then
echo "🔄 Already running, skip"
exit 0
fi
export FRONTMATTER_UPDATE_RUNNING=1
# 実際の作業
node scripts/update-frontmatter-postcommit.js
# クリーンアップ(実際は不要、プロセス終了時に環境変数も消える)
unset FRONTMATTER_UPDATE_RUNNING
このアプローチはシンプルですが確実な解決策です。プロセスレベルで状態を管理することで、再帰呼び出しを効果的に防止できました。複雑なフラグ組み合わせやGit内部メカニズムに依存せずに問題を解決できる点で実用的な選択でした。
すべての機能が正常に動作するように見えましたが、生成されたフロントマターを確認したときにまた別の問題が発見されました。
messages:
- "\U0001F680 新機能追加"
- "\U0001F41B バグ修正"
コミットメッセージの絵文字がユニコードエスケープシーケンスに変換されて保存されていました。
gray-matterは内部的にjs-yamlを使用しますが、stringifyプロセスで独自の処理を追加します。デバッグの結果、gray-matterのstringifyメソッドがユニコード文字をエスケープすることを確認しました。
これはYAMLスペック上有効な表現ですが、可読性とユーザー体験の観点からは望ましくありませんでした。GitHubのissueと関連文書を調査した結果、次のような解決策を導き出しました:
import yaml from 'js-yaml';
import matter from 'gray-matter';
// パースはgray-matterで(これは問題なし)
const { data, content } = matter(fileContent);
// 保存はjs-yamlで直接!
const yamlStr = yaml.dump(data, {
lineWidth: -1,
noRefs: true,
sortKeys: false
});
// フロントマター再組み立て
const updatedContent = `---\n${yamlStr}---\n${content}`;
このアプローチは各ライブラリの長所を活用する実用的な解決策です。gray-matterの便利なパース機能は維持しながら、js-yamlのより柔軟なstringifyオプションを活用して望む出力形式を得ることができました。
システムが完成した後、重要な設計決定を下す必要がありました。自動化システムがユーザーが手動で設定したメタデータを上書きしないようにすることでした。
---
title: 私の投稿
category: blog # これは消えてはいけない...
tags: [git, automation] # これも...
featured: true # これも重要...
---
フロントマターには様々なメタデータが含まれることができ、これらの一部は自動化できますが、一部はユーザーの明示的な意図を反映する必要があります。
次のような明確なポリシーを確立しました:
自動管理対象:
手動管理対象:
const AUTO_MANAGED_FIELDS = [
'title', // H1から自動抽出
'description', // 最初の段落から自動抽出
'authors', // git logから
'dates', // git logから
'messages' // git logから
];
// 既存データから自動フィールドのみ削除
AUTO_MANAGED_FIELDS.forEach(field => delete data[field]);
// 新しく抽出したデータをマージ
Object.assign(data, newData);
このような選択的更新戦略を通じて、自動化の利便性とユーザー制御権の間のバランスを達成できました。
---
title: 手動でコピーペースト
description: これも手動で作成
authors:
- えっと...私のGitHub IDは何だっけ?
dates:
- '2025-01-... 何日だったかな?'
---
git add posts/my-post.md
git commit -m "📝 ブログ投稿作成"
# 終了。本当に終了。
フロントマターは自動的に:
---
title: Gitヒストリーベースのフロントマター自動化システム構築
description: "また日付更新忘れた..." 金曜日午後4時...
authors:
- xiyo
dates:
- '2025-07-13T19:24:00+09:00'
messages:
- 📝 ブログ投稿作成
category: blog # これは私が設定したのでそのまま維持
---
初期予想とは異なり、全体の実装には約6時間かかりました:
しかし、この投資は長期的に相当な時間節約効果をもたらしました。毎コミットごとに30秒〜1分程度かかっていたメタデータ更新作業が完全に自動化され、人的エラーの可能性も排除されました。
この自動化システムの核心価値は単純な時間節約を超えています。開発者がメタデータ管理という付随的な作業から解放されて、本質的なコンテンツ作成に集中できるようになったという点が最も重要です。
繰り返し的な手作業は単に時間を消費するだけでなく、創造的な作業の流れを妨げます。このような自動化を通じて、開発者はより価値のある作業に時間とエネルギーを投資できるようになります。
/scripts/update-frontmatter-postcommit.js /.husky/post-commit