コンプライアンスを「記憶」ではなくビルドで止める — AI量産時代のコンテンツ品質ゲート実装
AI で記事を数十本生成すると、ボトルネックは執筆から審査に移ります。薬機法・ステマ規制・NG表現・監修者必須をレビュアーの記憶で守る運用は破綻します。スキーマ検証・型ゲート・CI の3層で規制違反をビルド失敗にする実装を、複数メディアで実証した型として示します。
by Akisame
「AI で記事を量産すると、最初に何が壊れるのか?」——品質ではありません。審査です。薬機法のような領域固有の広告規制・ステマ規制・NG断定表現・監修者必須といった表記ルールを「レビュアーが気をつける」で守る運用は、本数と改稿頻度に比例して必ず破綻します。本稿は、違反を「気をつける」から「ビルドが落ちるので出荷できない」に変える3層ゲート——frontmatter スキーマ・TypeScript の型・CI の本文検査——の実装を、規制表現の制約が強い領域の個人運営メディア複数で実証した型として書きます。
この記事は実装に寄せた [lab] 記事です。「AI が書いた記事を Google がどう扱うか・人間レビューの役割が品質をどう分けるか」というビジネス側の議論は、対になる 「AIが書いた記事」をGoogleは嫌うのか:実測データで検証 を参照してください。本稿はあちらの結論——人間レビューの役割設計が分水嶺になる——を、「レビューを制度=ゲートに落とす」実装で受けるものです。
なぜ「レビューで気をつける」は破綻するか
前提となる規制の概観だけ、一次資料の粒度で押さえておきます。景品表示法は、商品・サービスの内容を実際より良く見せる優良誤認表示、取引条件を過度に有利に見せる有利誤認表示などの不当表示を禁止しており、違反への措置として措置命令や課徴金納付命令の制度が定められています。また2023年10月1日からは、いわゆるステマ規制として「一般消費者が事業者の表示であることを判別することが困難である表示」が規制対象に加わりました。このステマ規制で規制を受けるのは商品・サービスを供給する**事業者(広告主)**であり、依頼を受けたインフルエンサー等の第三者は規制の対象とならない、と消費者庁は示しています(出典はいずれも消費者庁・2026-07-07 確認)。
つまり、AI に書かせようが外部ライターに書かせようが、表示の責任は媒体を運営する事業者側に残る。ここが出発点です。
では、その責任をどう果たすか。チェックリストを作り、公開前にレビュアーが確認する——これが標準回答で、記事が月に数本ならそれで回ります。破綻するのは量産に入ってからです。構造は組合せ爆発で説明できます。
- 本数: AI で生成すれば数十本が一晩で出てくる。審査だけが人間の逐次処理のまま残る。
- 改稿: 公開後のリライトのたびに「全チェック項目をもう一度」は実行されない。初回だけ厳格で、更新はザルになる。
- 書き手の混在: 人間+複数の AI+外部入稿。それぞれがルールの別バージョンを(あるいは何も)参照して書いてくる。
このとき起きるのは「チェックリストが無い」事故ではありません。チェックリストは存在するのに事故る。実行されない・版が古い・「今回だけ」の例外が黙認される——運用でカバーの三重奏です。人の注意力は本数に対してスケールしないので、これは担当者の資質の問題ではなく構造の問題です。
解き方の発想を変えます。出荷経路の上に置いた検査だけが、実行を保証される。レビュアーの記憶は迂回できますが、git push から本番反映までの経路上にある検査は迂回できません。ビルドが落ちれば出荷できない——この物理的な事実に規制要件を翻訳するのが、以下の3層です。
| 層 | 止める場所 | 止めるもの |
|---|---|---|
| 層1 | コンテンツスキーマ(Velite) | frontmatter で表現できる違反(監修者なし公開・PR 表記の不整合) |
| 層2 | TypeScript の型 | コンプラ表記コンポーネントの書き忘れ・props 欠落 |
| 層3 | CI スクリプト | 本文の NG 表現・壊れた内部リンク |
層1: frontmatter スキーマで止める — Velite × superRefine
最初の層は、規制要件を frontmatter の構造に翻訳することです。「監修者を必ず入れる」という運用ルールは、「draft: false の記事に reviewer が無ければスキーマ検証で落ちる」という構造に書き直せます。MDX-in-git + Velite の構成なら、スキーマは velite.config.ts にあり、検証はビルドの一部として必ず走ります。
単一フィールドの必須化だけなら s.string() で足りますが、コンプラ要件はたいていフィールド間の整合です(「PR 記事なら表記必須」)。ここで superRefine を使います。
// velite.config.ts(抜粋)— Velite の s は zod 互換
const posts = defineCollection({
name: "Post",
pattern: "posts/**/*.mdx",
schema: s
.object({
title: s.string().max(120),
draft: s.boolean().default(false),
pr: s.boolean().default(false), // 広告主から対価を得ている記事か
reviewer: s.string().optional(), // 監修者ハンドル
disclosureCta: s.boolean().default(false), // PR表記つきCTAを本文に置いたか
})
.superRefine((data, ctx) => {
// ルール1: 公開(非draft)記事は監修者必須
if (!data.draft && !data.reviewer) {
ctx.addIssue({
code: "custom",
path: ["reviewer"],
message:
"公開記事には reviewer(監修者)が必須です。draft: true に戻すか監修者を設定してください。",
});
}
// ルール2: PR記事なのに表記が無い/非PRなのに表記だけある、の不整合を止める
if (data.pr !== data.disclosureCta) {
ctx.addIssue({
code: "custom",
path: ["pr"],
message:
"pr と disclosureCta が不整合です。PR記事はPR表記CTAが必須、非PR記事にPR表記は置けません。",
});
}
}),
});このロジックは zod 単体(Velite の s は zod 互換)に切り出し、合成の frontmatter 5 ケースで検証しました。
OK draft は監修者なしで通る
OK 公開なのに監修者なし → 落ちる — 公開記事には reviewer(監修者)が必須です。…
OK 公開 + 監修者 → 通る
OK PRなのに表記なし → 落ちる — pr と disclosureCta が不整合です。…
5/5 passed設計上のポイントは2つです。第一に、ワークフローの状態(draft)を条件に使うこと。「下書きは自由に、公開は厳格に」をスキーマで表現すると、執筆の速度を殺さずに出口だけ締められます。第二に、ルール2のような双方向の整合チェックにすること。「PR なのに表記なし」だけでなく「非 PR なのに表記あり」も止めると、コピペで frontmatter を使い回したときの事故(前の記事の PR フラグが残る)が両方向で検出されます。
層2: 型で止める — 必須コンポーネントと props
層1は frontmatter、つまり宣言を検査します。しかし宣言と本文の実体はずれます。disclosureCta: true と書いたのに本文に表記が無い、あるいは表記コンポーネントを置いたが肝心の広告主名を書き忘れた——これを止めるのが型の層です。
コンプラ表記(PR 表記・免責)は自由記述にせずコンポーネント化し、必須情報を required props にします。
type DisclosureProps = {
sponsor: string; // 広告主名 — optional にしない
relationship: "affiliate" | "paid" | "supplied"; // 対価の種別
};
export function Disclosure({ sponsor, relationship }: DisclosureProps) {
const label = { affiliate: "アフィリエイト", paid: "タイアップ", supplied: "商品提供" }[relationship];
return (
<p role="note">
本記事は {sponsor} との{label}を含みます(PR)。
</p>
);
}
// PR記事レイアウトは Disclosure の「実体」を props で要求する。
// ただの ReactNode では型が合わない = 表記の書き忘れがコンパイルエラーになる
type PrArticleLayoutProps = {
disclosure: React.ReactElement<DisclosureProps, typeof Disclosure>;
children: React.ReactNode;
};
export function PrArticleLayout({ disclosure, children }: PrArticleLayoutProps) {
return (
<article>
{disclosure}
{children}
</article>
);
}PrArticleLayout が disclosure を required prop として要求するので、「PR 記事のレイアウトを使ったのに表記を渡し忘れる」がコンパイルエラーになります。tsc --strict で確認すると、正しい使い方は通り、disclosure の渡し忘れも sponsor の書き忘れも型エラーとして落ちます(@ts-expect-error を置いた検証ファイルで、エラーが期待どおりの2箇所で出ることを確認済み)。文言の統一という副次効果もあります——表記の文面がコンポーネント1箇所に集約されるので、「表記ガイドラインの改定」がコード1行の変更になります。
層3: 本文を検査する — content-check スクリプト
frontmatter と型で止まらないもの——本文の文字列そのもの——は、CI のスクリプトで検査します。対象は機械的に判定できるものに絞ります。代表は3つです。
- NG 表現辞書: 断定的な効果保証(「必ず〜」)・根拠表記のない最大級表現(「No.1」)など、優良誤認の火種になりやすい定型パターン
- 内部リンク解決: リンク先の記事が存在するか(改稿・削除で静かに壊れる)
- 必須セクション: 記事種別ごとの必須要素の有無
// scripts/content-check.mjs — CI で実行し、違反があれば exit 1
import fs from "node:fs";
import path from "node:path";
const CONTENT_DIR = process.argv[2] ?? "content";
// NG表現辞書(汎用例)。pattern と、誤検知を吸収する allow(文脈例外)を持たせる
const NG_DICT = [
{ id: "N01", pattern: /必ず(儲かる|痩せる|治る|上がる)/, reason: "断定的な効果保証", allow: [] },
{ id: "N02", pattern: /(日本一|世界一|No\.?1)/i, reason: "最大級表現(根拠表記が必要)", allow: [/調査概要/] },
{ id: "N03", pattern: /副作用(は|が)?(ない|ありません)/, reason: "安全性の断定", allow: [] },
];
const files = fs.readdirSync(CONTENT_DIR).filter((f) => f.endsWith(".mdx"));
const slugs = new Set(files.map((f) => path.basename(f, ".mdx")));
const violations = [];
for (const file of files) {
const text = fs.readFileSync(path.join(CONTENT_DIR, file), "utf8");
text.split("\n").forEach((line, i) => {
if (line.trimStart().startsWith("```")) return; // 最小実装: コードフェンス行は無視
for (const rule of NG_DICT) {
if (rule.pattern.test(line) && !rule.allow.some((a) => a.test(line))) {
violations.push(`${file}:${i + 1} [${rule.id}] ${rule.reason}: ${line.trim().slice(0, 40)}`);
}
}
for (const m of line.matchAll(/\]\(\/(insights|lab)\/([a-z0-9-]+)\)/g)) {
if (!slugs.has(m[2])) violations.push(`${file}:${i + 1} [LINK] 内部リンク先が存在しない: /${m[1]}/${m[2]}`);
}
});
}
if (violations.length > 0) {
console.error(`content-check: ${violations.length} violation(s)`);
for (const v of violations) console.error(" " + v);
process.exit(1);
}
console.log(`content-check: OK (${files.length} files)`);デモ用に用意した合成の MDX(違反を仕込んだ1本+通る2本)に対して走らせた結果です。
content-check: 3 violation(s)
bad-article.mdx:4 [N01] 断定的な効果保証: このサプリで必ず痩せる。売上No.1。
bad-article.mdx:4 [N02] 最大級表現(根拠表記が必要): このサプリで必ず痩せる。売上No.1。
bad-article.mdx:5 [LINK] 内部リンク先が存在しない: /lab/no-such-articlefile:行番号 形式で出すのは、CI のログから該当行に一発で飛べるようにするためです。運用が長くなるほど効くのは、辞書に allow(文脈例外)を持たせている部分です。NG 表現の検査は必ず誤検知と付き合うことになります——「No.1」は原則 NG でも、「調査概要」を明記した行なら根拠つき表記として通したい。この例外をレビュアーの頭の中ではなく辞書の構造に持たせることで、「これは例外だから」の口頭運用(=ゲートの静かな形骸化)を防ぎます。例外にも ID とルールがある状態を保つ、と言い換えてもいいです。
入口の正規化 — 外部生成 MDX の表記ゆれを決定的に吸収
3層ゲートを立てると、次の問題が顕在化します。AI や外部ライターから届く MDX 数十本が、ゲート以前の表記ゆれで大量に落ちるのです。見出しレベルが H1 から始まる・コンポーネント名が微妙に違う(Note と Callout)・frontmatter のキー名がゆれる(writer と author)——1本ずつ手で直すと、その修正は再入稿のたびに無限に発生します。
答えは個別修正ではなく、入口に正規化スクリプトを1本置くことです。変換は1回書けば終わり、以後の入稿全部に効きます。
// scripts/normalize-intake.mjs — 外部生成 MDX の表記ゆれを入口で決定的に吸収
const ALIASES = [
// 見出し: H1 は本文に書かない規約 → H2 に落とす
{ re: /^# (?!#)/gm, to: "## " },
// コンポーネント名のゆれ → 正規名へ
{ re: /<Note>([\s\S]*?)<\/Note>/g, to: '<Callout type="note">$1</Callout>' },
// frontmatter キーのゆれ → 正規キーへ
{ re: /^writer:/m, to: "author:" },
];
export function normalize(src) {
return ALIASES.reduce((text, { re, to }) => text.replace(re, to), src);
}ゲートと正規化は役割が対になっています。機械的に直せるゆれは入口で黙って直し(正規化)、判断が要る違反だけを人間に返す(ゲート)。この分担にすると、ゲートのエラーは「対応が必要な本物」だけになり、書き手(人間でも AI でも)への差し戻しが減ります。逆にこの分担がないと、ゲートが些末な表記ゆれで鳴り続け、オオカミ少年化して本物の違反が流されます。
なお、書き手が AI の場合は正規化ルールをそのまま生成側の指示書に還流できます。「H1 を書かない・コンポーネントは Callout」と最初から指示すれば正規化の仕事自体が減る——入口の変換ログは、生成プロンプトの改善データでもあります。この「AI の生成物を検証可能な形に整えてから運用に載せる」考え方は、Claude Codeでマーケティング業務を自動化する実践ガイド で書いた自動化の設計原則と同型です。
運用 — ゲートと人間レビューの分担
最後に、この仕組みが何を守らないかを明確にします。3層ゲートが守れるのは、機械的に判定可能な違反だけです。
| 例 | 守る主体 | |
|---|---|---|
| 機械が判定できる | 監修者なし公開・PR 表記の欠落・NG 定型表現・壊れたリンク | ゲート(層1〜3) |
| 人間にしか判定できない | 文脈依存の誇張(個々の語は適法でも全体印象が誤認を招く)・事実の正確性・引用の適切さ | 人間レビュー |
危険なのは、ゲートを入れたことで「機械が通したから大丈夫」と人間レビューが形骸化することです。実際は逆で、ゲートの目的は人間の注意力を、機械が判定できない領域に全額振り向けることにあります。表記の有無やリンク切れの確認に使っていた集中力を、文章全体が与える印象・事実の裏取りという、規制上も品質上も本丸の判断に回す。ゲートはレビューの代替ではなく、レビューの前処理です。
これは対になる insights 記事 「AIが書いた記事」をGoogleは嫌うのか の結論——AI 生成そのものではなく、人間レビューの役割設計が品質を分ける——の実装形でもあります。あちらで「人間の役割」と呼んだものを、本稿は「機械に任せる部分を明示的に切り出した残り」として定義しました。そして、AI エージェントが実務の生成部分を担うほど、人間側の価値が検証と判断の設計に移っていくという話は AIエージェント時代に生き残るマーケターのスキルセット に書いたとおりです。コンテンツ運用はその縮図で、書く力より「何を機械で保証し、何を人間が見るか」を設計する力が、量産時代の編集の中核スキルになっています。
まとめます。AI 量産時代のコンプライアンスは、レビュアーの記憶と注意力ではなく、出荷経路上の構造で守る。frontmatter スキーマで宣言を、型で実体を、CI で本文を検査し、入口の正規化で誤検知を減らし、人間の目は機械に判定できない本丸へ。規制違反が「ビルドが落ちるので出荷できない」になったとき、初めてチェックリストは運用に依存しない制度になります。