自動更新サイトに「捏造セールを弾く」検証ゲートをビルドに入れる(実装コード付き)

セール情報サイトを日次で自動更新していると、機能追加よりも先に効いてくるのが「載せてはいけないものを載せない」仕組みだ。価格や割引率は生モノで、しかも自動生成の文章は平然ともっともらしい嘘を書く。運用している ocha.autoarticles.net(お茶)、menrui.autoarticles.net(麺類)、manga-deals(Kindleセール)では、この問題を「ビルドを止める」という一点に集約して解いている。

方針:レビューではなくビルドで落とす

原稿の内容を目視レビューで担保する運用は、1日2回の自動更新には乗らない。そこで、データ(data/articles.json)から静的HTMLを生成する build.js の入口に検証関数を置き、条件を満たさない記事があれば process.exit(1) する。デプロイはビルド成功が前提なので、検証に落ちたデータは本番に出ない。

掲載を許すのは次の2種類だけ、というのが設計の出発点だ。

  • (A) 実在が確認できた具体セール — 確認日(confirmed)と出典URL(source)が揃っているもの
  • (B) 恒常的な仕組みの解説 — 断定的な割引率・価格を書かないもの

つまり「断定表現を書くなら裏付けを出せ、出せないなら断定するな」という一本のルールに落ちる。

実装:断定表現を正規表現で検出する

判定は記事オブジェクト全体を JSON.stringify した文字列に対してかける。タイトルだけ、本文だけ、と対象を絞ると deal.detailhighlights の書き漏れを見逃すためだ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 「50%OFF」「半額」「◯◯円→◯◯円」「全巻99円」等、
// 特定商品の割引・価格の断定を疑うパターン
const assertive =
/(\d{1,3}\s*%\s*(OFF|オフ|還元(?!率))|半額|実質\s*無料|今だけ半額|→\s*\d+\s*円|\d+\s*円均一|全巻\s*\d+\s*円|各\s*\d+\s*円)/;

function validateArticles(articles) {
const problems = [];
const isUrl = (s) => typeof s === "string" && /^https?:\/\//.test(s);

for (const a of articles) {
const blob = JSON.stringify(a);
const sources = []
.concat(a.source || [], a.sources || [], a.sourceUrl || [])
.filter(Boolean);
const hasSource = sources.some(isUrl);

// 断定的な割引/価格を含むなら confirmed と source の両方が必須
if (assertive.test(blob) && (!a.confirmed || !hasSource)) {
problems.push(
` ✗ ${a.slug}: 断定的な割引/価格表現がありますが、confirmed(確認日) と 出典URL(source) の両方が必要です`
);
}
if (!a.confirmed && !a.date) {
problems.push(` ✗ ${a.slug}: confirmed/date(確認日)がありません`);
}
if (sources.length && !hasSource) {
problems.push(` ✗ ${a.slug}: source が有効なURLではありません`);
}
}

if (problems.length) {
console.error("セール実在検証に失敗しました:\n" + problems.join("\n"));
process.exit(1);
}
}

正規表現のいくつかは実際の事故から足したものだ。還元(?!率) の否定先読みがその例で、「ポイント還元率の仕組み」という一般解説(断定ではない)が毎回ゲートに引っかかっていたので、「還元率」という語だけ除外した。この手のゲートは、厳しすぎると正当な記事が通らず、運用者が検証そのものを外したくなる。誤検知を潰しながら締めるのが実務的な落としどころになる。

出力側にも同じ前提を通す

検証を通ったあとの生成側でも、確認日と出典を必ず可視化する。confirmed があれば「調査日:7月19日時点」と表示し、出典URLがあれば一覧としてレンダリングしたうえで、注記の文面を出典の有無で切り替える。

1
2
3
4
5
6
const confIso = a.confirmed || a.date;
const sourceList = [].concat(a.source || [], a.sources || []).filter(Boolean);

const note = sourceList.length
? `※本記事の価格・割引・セール期限は、上記の出典を基に調査日(${jday(confIso)}時点)に確認した内容です。変動し、予告なく終了する場合があります。`
: `※本記事は特定商品の割引率・価格を断定するものではありません。対象商品・条件・期間は変動します。`;

読者から見た価値もここにある。「いつ時点の情報か」「どこで確認したか」が各ページに出ていれば、鮮度の判断を読者に委ねられる。逆にこれを書かないサイトは、正しい情報でも信用できない。

sitemap の lastmod も同じ値から引く

確認日を単一の真実にしておくと、副次的に SEO 側の整合も取れる。sitemap.xmllastmoda.confirmed || a.date から生成しておけば、記事の実際の確認日とサイトマップの更新日がずれない。未来日付の lastmod を作ってしまう事故も、確認日が実行日を超えない限り起きない。

1
2
3
4
const urls = articles.map((a) => ({
loc: `${BASE}/articles/${a.slug}/`,
lastmod: a.confirmed || a.date,
}));

多層で守る

同じ考え方を、脆弱性まとめの cve.autoarticles.net では別の条件で実装している。あちらの掲載基準は「severity が High/Critical」「CVSS >= 7.0」「有効な source URL 必須」で、満たさなければ同じくビルドが止まる。扱うデータが違えばゲートの条件も違うが、「データの入口とビルドの2段で弾く」という構造は共通だ。

自動更新の運用でいちばん効いたのは、ルールを文章で書くことではなく、ルールを破ったらデプロイできない状態にすることだった。文章のルールは守られないことがあるが、process.exit(1) は必ず守られる。


自動更新サイトに「捏造セールを弾く」検証ゲートをビルドに入れる(実装コード付き)
https://blog.hashito.biz/2026/07/19/fleet-sale-sites-build-validation-guard/
著者
hashito
作成日
2026年7月19日
著作権