ブログ記事のURLやカテゴリー、タグ付けを自動化した話
はじめに
ブログの記事を書くたびに
- 適切なファイル名を考える
- カテゴリーを決める
- タグを付ける
という作業が発生し、地味にめんどくさいなぁ・・・って思ったり、そもそもタグ付けするのを忘れたりしていました。
今までの記事とタグの表記揺れを減らしたりなど一貫性を保つのも地味に大変で、2025年にもなってこれらの作業を手動で行うのはどうかなって思ったので、 ブログのGitHub Actions整備のついでにメタデータ付与の作業を自動化してみました。
システムの概要
構成要素
-
自動メタデータ生成ワークフロー (
.github/workflows/autogen-metadata.yml) -
Go製のメタデータ生成スクリプト (
scripts/autogen-metadata/cmd/main.go) -
メタデータベース (
data/metadata-database.json)
動作の流れ
新記事の自動処理
- プルリクエストで
_posts/**/*.mdファイルが変更される - ファイル名が
*-*-*-NewPost*.mdの形式かチェック - 該当ファイルをGemini-2.5 Flash-liteで分析
- メタデータベースを参照してカテゴリー、タグ、英語ファイル名を自動生成
- フロントマターを更新してコミット
- プルリクエストにコメントで結果を報告
技術的な話
メタデータベース
システムは既存の記事からメタデータを抽出してデータベースを構築し、一貫性のあるカテゴリーとタグの管理を行います。
// シンプルなメタデータベースの構造
type SimpleMetadataDatabase struct {
Categories struct {
List []string `json:"list"`
} `json:"categories"`
Tags struct {
List []string `json:"list"`
} `json:"tags"`
}
データベースの特徴
- シンプルな構造: カテゴリーとタグのリストを保持
- 一貫性の保証: 既存のカテゴリーとタグを優先使用
- 拡張性: 新しいカテゴリーとタグを動的に追加可能
Gemini-2.5 Flash-liteの活用
// モデルを取得(Gemini 2.5 Flash-liteを使用)
model := client.GenerativeModel("models/gemini-2.5-flash-lite")
// 生成設定
model.GenerationConfig.Temperature = genai.Ptr(float32(0.3))
model.GenerationConfig.TopK = genai.Ptr(int32(40))
model.GenerationConfig.TopP = genai.Ptr(float32(0.95))
model.GenerationConfig.MaxOutputTokens = genai.Ptr(int32(2048))
Gemini-2.5 Flash-liteは軽量で高速、かつコスト効率が良いモデルです。記事の内容を分析して、適切なメタデータを生成してくれます。
プロンプト設計
prompt := fmt.Sprintf(`Please analyze the following Japanese blog article and return a JSON response in the specified format:
%s
{
"english_filename": "english_filename",
"category": "category in Japanese",
"tags": ["tag1", "tag2", "tag3"],
"description": "SEO-friendly description under 160 chars for meta tag in Japanese"
}
Article:
%s
Available Categories: %s
Available Tags: %s
Rules:
- Return ONLY the JSON object, no explanations or markdown formatting
- Use existing categories/tags when possible
- If existing metadata is provided, consider it but you can override if the content suggests better categorization
- English filename should be descriptive and use snake_case
- Category should be one of the available categories
- Tags should be a mix of available tags and content-specific tags (max 5 tags)
- Description should be SEO-friendly, concise, and under 160 characters for HTML meta description tag
JSON:`, existingInfo, content, categoriesStr, tagsStr)
既存のカテゴリーとタグのデータベースを参照して、一貫性を保ちながら新しいメタデータを生成します。また、SEOに最適化された説明文も生成します。
メタデータベースの活用
構築されたメタデータベースには以下の情報が含まれています:
- カテゴリー: 既存の記事から抽出されたカテゴリーリスト
- タグ: 既存の記事から抽出されたタグリスト
- 一貫性の保証: 既存のカテゴリーとタグを優先使用することで、ブログ全体の一貫性を維持
このデータベースを参照することで、Gemini-2.5 Flash-liteは既存のブログの一貫性を保ちながら、適切なメタデータを生成できます。
実装のポイント
1. ファイル名の自動生成
// generateFilename はメタデータに基づいて新しいファイル名を生成します
func (c *MarkdownClient) generateFilename(originalFilename string, metadata *model.PostMetadata) string {
// 元のファイルのディレクトリと拡張子を取得
dir := filepath.Dir(originalFilename)
ext := filepath.Ext(originalFilename)
// タイトルからファイル名に適した文字列を生成
title := strings.TrimSpace(metadata.Title)
if title == "" {
// タイトルが空の場合は元のファイル名を使用
return originalFilename
}
// タイトルをファイル名に適した形式に変換
// スペースをハイフンに置換し、特殊文字を除去
titleForFilename := strings.ToLower(title)
titleForFilename = strings.ReplaceAll(titleForFilename, " ", "-")
titleForFilename = regexp.MustCompile(`[^a-z0-9\-]`).ReplaceAllString(titleForFilename, "")
// 連続するハイフンを単一のハイフンに置換
titleForFilename = regexp.MustCompile(`-+`).ReplaceAllString(titleForFilename, "-")
// 先頭と末尾のハイフンを除去
titleForFilename = strings.Trim(titleForFilename, "-")
// 新しいファイル名を生成
newFilename := filepath.Join(dir, titleForFilename+ext)
return newFilename
}
2. フロントマターの更新
// createFrontMatter はPostMetadataからYAMLのFrontMatterを作成します
func (c *MarkdownClient) createFrontMatter(metadata *model.PostMetadata) (string, error) {
// FrontMatter構造体を作成
frontMatter := model.FrontMatter{
Title: metadata.Title,
Category: metadata.Category,
Tags: metadata.Tags,
Description: metadata.Description,
}
// YAMLにマーシャル
yamlData, err := yaml.Marshal(frontMatter)
if err != nil {
return "", fmt.Errorf("YAMLマーシャリングに失敗しました: %w", err)
}
return string(yamlData), nil
}
システムは既存のタイトルを保持しながら、新しいカテゴリー、タグ、説明文を追加します。これにより、記事のタイトルは変更されず、メタデータのみが更新されます。
3. エラーハンドリング
// セーフティフィルターやその他の制限が原因の可能性をチェック
candidate := resp.Candidates[0]
if candidate.FinishReason == genai.FinishReasonSafety {
return nil, fmt.Errorf("%s レスポンスがセーフィフィルターによってブロックされました: %w", logPrefix, err)
}
text := ""
for _, part := range candidate.Content.Parts {
if textPart, ok := part.(genai.Text); ok {
text += string(textPart)
}
}
if text == "" {
return nil, fmt.Errorf("%s レスポンステキストが空です, 終了理由: %v", logPrefix, candidate.FinishReason)
}
Gemini APIの各種エラーケースに対応し、適切なエラーメッセージを返すようにしています。また、UTF-8の検証やJSONのクリーニングも行います。
使用例
プルリクエストでの動作
-
2025-09-25-NewPost.mdというファイルを作成 - プルリクエストを作成
- GitHub Actionsが自動実行
- 以下のような結果が生成されます:
{
"processed_files": [
{
"original_file": "_posts/2025/09/2025-09-25-NewPost.md",
"new_file": "_posts/2025/09/blog-automation-with-gemini.md",
"category": "ブログ",
"tags": ["Jekyll", "GitHub Actions", "Gemini", "自動化", "備忘録"],
"updated": true
}
],
"total_files": 1,
"updated_files": 1
}
プルリクエストコメント
システムは自動的にプルリクエストにコメントを投稿し、処理結果を報告します:
## 🤖 自動メタデータ生成結果
### 📊 処理結果
- **処理ファイル数**: 1
- **更新ファイル数**: 1
### 📝 処理されたファイル
1. ✅ `_posts/2025/09/2025-09-25-NewPost.md` → `_posts/2025/09/blog-automation-with-gemini.md`
- カテゴリ: ブログ
- タグ: Jekyll, GitHub Actions, Gemini, 自動化, 備忘録
---
*このコメントは自動生成されました*
メリット
1. 作業効率の向上
- 手動でのメタデータ設定が不要
- 一貫性のあるファイル名とタグ付け
- プルリクエストベースの自動化
2. 品質の向上
- AIによる適切なカテゴリー分類
- 既存のタグデータベースとの整合性
- 英語ファイル名によるSEO向上
3. コスト効率
- Gemini-2.5 Flash-liteの軽量モデル使用
- 必要な時のみAPI呼び出し
- 無料枠内での運用可能
今後の改善予定
- アイキャッチ画像自動生成: nano bananaを使用した画像生成の統合
まとめ
Gemini-2.5 Flash-liteを使った自動化システムにより、ブログ記事のメタデータ管理が大幅に効率化されました。このシステムは1つのワークフローで構成されています:
システムの特徴
- メタデータベースの活用: 既存記事から抽出したカテゴリーとタグを参照し、一貫性のあるデータベースを活用
- AIによる自動生成: Gemini-2.5 Flash-liteが記事内容を分析し、適切なメタデータを生成
- 完全自動化: プルリクエストベースで、手動介入なしにメタデータを更新
効果
- 作業効率の向上: 手動でのメタデータ設定が不要
- 品質の向上: AIによる適切なカテゴリー分類と一貫性のあるタグ付け
- コスト効率: 軽量モデルの使用により、無料枠内での運用が可能
ぶ!ログ