AIレビューで磨くリントシステム設計:ブログメタデータ検証の実装のサムネイル

はじめに

記事数が増えるとカテゴリのタイポ、descriptionの書き忘れ、許可されていないタグ名といったミスが蓄積します。
scripts/lint-blog-metadata/ をprebuildフローに組み込み、spec.yaml を唯一の許可値として参照することで、メタデータ品質を自動保証できます。

ブログのメタデータ管理でこんなことありませんか?

  • カテゴリのタイポやタグの不整合、descriptionの書き忘れといったミスが蓄積している。
  • 記事数が増えるにつれて手作業での管理が限界を迎え、品質の低下が目立ち始めた。
  • サイト内の情報が整理されず、ユーザー体験やSEOに悪影響を及ぼす負債を感じている。

この記事をお勧めしない人

  • メタデータの品質が多少低くても、コンテンツの本質には影響しないと考える人。
  • 手作業でのダブルチェックが、自動化されたシステムより信頼できると信じる人。
  • AIとの協調設計や、ビルドプロセスを最適化するアーキテクチャに興味がない人。

もし一つでも当てはまらないなら、読み進める価値があるかもしれません。

手作業チェックを続けると

  • 記事数が増えるほど見落としが増え、タグの不整合で関連記事フィルタが壊れる。
  • カテゴリ名をリネームするたびに全記事を手作業で確認する必要が生じる。
  • AIに記事を書かせると spec.yaml で定義していない値が混入し、気づかないままビルドが通る。

品質自動保証という明るい未来

  • この記事を読めば、RuleEngine クラスとプラグイン形式の検証ルールで構成されるリントシステムの設計思想が手に入る。
  • 具体的には、spec.yaml から許可カテゴリ・タグを動的に読み込み、prebuildで全記事を検証する設計図を手に入れられる。
  • この方法は、本ブログ自身のメタデータ品質を担保するために実証済みであり、AIレビューで設計を改善したプロセスも公開する。

私も同じでした

このブログでも、記事が増えるにつれてカテゴリのタイポや description の空欄が蓄積していました。scripts/lint-blog-metadata/engine.jsprebuild スクリプトに組み込み、コンソールにはサマリー、詳細は tests/lint/blog-metadata-report.md に出力する形式にすることで解決しました。二段階出力の設計はAIのレビューで気づいた改善点です。このリントシステムの前提となる spec.yaml の構造設計——3段階の階層型マージによりカテゴリやラベルを一元管理する仕組み——が整っていたからこそ、「spec.yamlを更新するだけで検証ルールが反映される」を実現できました。

開発の進捗

  • Before: ブログ記事のメタデータを手動でチェックしていたため、タグのタイポや description の不足などのミスが発生していました。
  • Current: プラグイン(※)形式のリントシステムを実装し、prebuild フロー(※)で全記事のメタデータを自動検証。エラーはコンソールサマリー(※)と Markdown レポート(※)で確認できます。
  • Next: 将来的には、画像の alt 属性チェックや内部リンクの検証など、コンテンツ品質全般に検証範囲を拡大予定です。

※ プラグイン : 機能を追加・変更しやすくする仕組み。
※ prebuild フロー : サイトを公開する前に行う一連の自動処理の流れ。
※ コンソールサマリー : 開発者向け画面に出る、チェック結果の要約。
※ Markdown レポート : チェック結果をまとめた、読みやすい報告書ファイル。

具体的なタスク

  • Before:
    既存のリントシステムのアーキテクチャ(※)を調査し、一貫性のある設計方針を策定しました。
  • Current:
    複数の検証ルール(必須フィールド、カテゴリ、タグ、日付形式、slug 形式、description 長)を実装し、spec.yaml(※)から許可値を動的に読み込む仕組みを構築しました。
  • Next:
    実際のブログ記事にリントを適用し、検出された問題を修正しました。

※ アーキテクチャ : システム全体の設計図や骨組みのこと。
※ spec.yaml: サイトの設定(例: 使えるカテゴリ名など)を書いておく設定ファイル。

ここからは、実際にClaudeMixで実装した具体的な設計と実装コードを公開します。

プラグイン形式のRuleEngineクラス実装、spec.yamlとの統合方法、AIレビューによる設計改善プロセス、そして二段階レポート機能の実装まで、ClaudeMixで実際に記述したJavaScriptコードを、ステップバイステップで解説します。

課題と解決策

ClaudeMix ブログは Cloudflare Workers 上で動作するため、 エッジ環境では動的な検証が困難 です。そのため、Markdown を事前に HTML へ変換する「プレビルド思想(※)」と同様に、メタデータ検証もビルド時に完了させる設計を目指しました。

また、プロジェクトの要件として、以下の3つの制約がありました。

  1. 既存 lint システムと同じアーキテクチャを採用する(学習コスト最小化)
  2. spec.yaml を Single Source of Truth(※)として動的に参照する
  3. 開発者体験を考慮した出力形式を実現する

※ プレビルド思想 : ユーザーがアクセスする「前」に、あらかじめページを作っておく考え方。
※ Single Source of Truth: 「信頼できる唯一の情報源」という意味。情報が1箇所にまとまっている状態。

工夫したこと

アーキテクチャの統一

既存の lint-template システムのコア設計を踏襲しました。具体的には、プラグイン形式の RuleEngine クラス(ルールを管理するプログラム)と、ルール登録機構を採用することで、開発者が直感的に理解できる構造を維持しました。

// scripts/lint-blog-metadata/core.js
class RuleEngine {
  constructor() {
    this.rules = new Map();
    this.config = null;
    this.results = [];
  }

  registerRule(name, rule) {
    this.rules.set(name, rule);
  }

  async checkFile(filePath) {
    const content = fs.readFileSync(filePath, 'utf8');
    const fileResults = [];

    for (const [ruleName, rule] of this.rules.entries()) {
      const ruleConfig = this.config?.rules?.[ruleName];
      if (ruleConfig && ruleConfig.enabled === false) continue;

      const ruleResults = await rule.check(content, filePath, ruleConfig || {});
      if (Array.isArray(ruleResults)) {
        fileResults.push(...ruleResults);
      }
    }
    return fileResults;
  }
}

spec.yaml 統合

js-yaml(※)を使って spec.yaml を読み込み、categories と tags の許可値を動的に取得しました。これにより、spec.yaml を更新するだけで検証ルールが自動的に反映される仕組みを実現しました。

// scripts/lint-blog-metadata/rules/metadata.js
'category-validation': {
  check: function(content, filePath, config) {
    const { data } = matter(content); // matterは記事のメタデータを読み取るライブラリ
    const specPath = path.join(process.cwd(), config.specPath);
    const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
    const allowedCategories = spec.categories.map(cat => cat.name);

    if (!allowedCategories.includes(data.category)) {
      results.push({
        message: `無効なカテゴリ: "${data.category}"`,
        suggestion: `許可されたカテゴリ: ${allowedCategories.join(', ')}`
      });
    }
  }
}

※ js-yaml: JavaScriptでYAML形式のファイルを読み書きするためのライブラリ(道具)。

ぶつかった壁

出力形式の設計

初期設計案では、コンソールに個別エラーも出力する設計でした。しかし、 AI(Claude)がレビューで「サマリーと詳細を分離すべき」という指摘 をしてくれました。

この指摘を受けて、以下のような二段階レポート形式に設計を変更しました。

  • コンソール : サマリーのみ表示(検査ファイル数、エラー数、警告数)
  • Markdown ファイル : ファイルごとにグループ化された詳細エラー

この変更により、開発者は概要を即座に把握し、必要に応じて詳細を確認できるようになりました。

解決方法

二段階レポートの実装

displayResults() メソッドでコンソールにサマリーを出力し、formatMarkdownReport() メソッドでファイルごとにグループ化された詳細エラーを Markdown に出力しました。

// scripts/lint-blog-metadata/engine.js
displayResults(results) {
  const summary = this.engine.getSummary();

  // コンソールにはサマリーのみ
  console.log('\n' + '='.repeat(50));
  console.log('📈 実行サマリー');
  console.log('='.repeat(50));
  console.log(`検査ファイル数: ${summary.files}`);
  console.log(`検出問題数: ${summary.total}`);
  console.log(`  エラー: ${summary.errors}`);
  console.log(`  警告: ${summary.warnings}`);

  // 詳細はMarkdownファイルに出力
  const markdownOutput = this.formatMarkdownReport(results, summary);
  const outputPath = path.join(process.cwd(), 'tests', 'lint', 'blog-metadata-report.md');
  fs.writeFileSync(outputPath, markdownOutput);

  console.log(`\n💾 Lint結果を ${outputPath} に保存しました`);
}

プレビルド志向の設計

エッジ環境の制約を踏まえ、 ランタイムでの動的検証ではなく、ビルド時の静的検証を採用 しました。これは、Markdown を事前に HTML へ変換するプレビルドシステムと同じ思想であり、エッジ環境でのパフォーマンスを最大化します。

// package.json
{
  "scripts": {
    "lint:blog-metadata": "node scripts/lint-blog-metadata/engine.js content/blog/posts", // メタデータをリントするコマンド
    "prebuild": "npm run lint:md && npm run lint:blog-metadata && node scripts/prebuild/generate-blog-posts.js"
  }
}

コード抜粋

最終的に実装した検証ルールの一例です。カテゴリ検証では、spec.yaml から動的に許可値を読み込み、記事のカテゴリが許可リストに含まれているかを確認します。

// scripts/lint-blog-metadata/rules/metadata.js
'category-validation': {
  name: 'category-validation',
  description: 'category の選択肢検証',
  severity: 'error',

  check: function(content, filePath, config) {
    const results = [];
    const { data } = matter(content);

    if (!data.category) { // categoryが未入力の場合は別のルールで検出するので、ここでは何もしない
      return results; // required-fieldsで検出されるのでスキップ
    }

    // spec.yaml からカテゴリ取得
    const specPath = path.join(process.cwd(), config.specPath || 'app/specs/blog/posts-spec.yaml');
    const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
    const allowedCategories = spec.categories.map(cat => cat.name);

    if (!allowedCategories.includes(data.category)) {
      results.push({
        message: `無効なカテゴリ: "${data.category}"`,
        line: 1,
        severity: config.severity || this.severity,
        file: filePath,
        rule: this.name,
        suggestion: `許可されたカテゴリ: ${allowedCategories.join(', ')}`
      });
    }

    return results;
  }
}

このコードの重要なポイントは、 spec.yaml を「信頼できる唯一の情報源(Single Source of Truth)」として扱っている 点です。カテゴリの追加や変更は spec.yaml を更新するだけで自動的に反映されるため、検証ルール自体を変更する必要がありません。

今回の学びと感想

今回の開発で最も印象的だったのは、 実装前にAIがレビューしてくれたことで、設計を改善できた という点です。

当初の設計案では「コンソールに詳細エラーも出力」という方針でしたが、AI が「開発者体験を考慮するなら、サマリーと詳細を分離すべき」と指摘してくれました。この一言がなければ、実装後に「見づらい」という問題に気づいて手戻りしていたかもしれません。

また、 プレビルド思想の徹底 により、エッジランタイムの制約を逆手に取ることができました。動的検証ができないという制約は、「すべてをビルド時に完了させる」という明確な設計指針を与えてくれました。

AIとの共同設計は、単なるコード生成ではありません。設計の壁打ち相手として、実装前に問題点を指摘してくれる存在として、非常に価値がありました。

同じ「spec.yamlを不変の契約とする」設計は、E2Eテストのセレクタ管理にも応用されています。extractTestIdパターンによるUIの文言とテストの分離を合わせて読むと、spec.yamlがコンテンツ・実装・テストの全レイヤーを統一するSSOT基盤として機能している全体像が見えてきます。

あなたのリポジトリへの適用

どちらを採用しますか?(1つ選択)

SAFETY|安全性を高める

ブログメタデータリントの検証ルールが最新のspec.yamlと同期しているか確認する

調査:

  1. scripts/lint-blog-metadata/配下のルール定義を確認せよ
  2. spec.yamlで許可されているカテゴリ・タグ一覧とリントルールが一致しているか確認せよ
  3. 最近追加したカテゴリ・タグがリントに反映されているか確認せよ

この内容をClaude Codeに貼り付けて実行します

SEPARATION|責務を分離

リントシステムのspec.yaml参照がSSOT原則に沿っているか点検する

調査:

  1. リントスクリプトがspec.yaml以外の場所から検証値を参照していないか確認せよ
  2. ハードコードされた許可値(カテゴリ名・タグ名等)がないか確認せよ
  3. プレビルドフロー(prebuild)でリントが実行されているか確認せよ

この内容をClaude Codeに貼り付けて実行します

外部コードのローカル実行にはリスクがあります。ブラウザ環境での実行を推奨します。