ブログ記事公開直前の自動QC — 6ヶ月間誤公開0件の秘密

1 min read · 327 words

活用テクニック / ブログ運営 / Python・自動化
約2,300文字

ブログを200記事以上運営していると、人が直接記事を検品しても必ず見落としが発生します。マークダウンの残り(太字がそのまま露出)、絵文字のホワイトリスト違反、出典の漏れ、空の表、ボックススタイルの残りなどです。そのため、私たちは記事がブログAPIに渡される直前に、自動でチェックして修正するステップを別途用意しました。

この記事では、その自動QCシステムをどのような意図で作り、どのように動作し、実際にどのような効果があったのか、そしてどのように検証したのかまでを解説します。同じ悩みを持つ運営者が、コード1ページ分で真似して作れるように要点だけをまとめました。

開発した理由

最初の1年間、私たちは2種類のトラブルを頻繁に目にしました。

1つ目は、モデル出力の残り。LLMで本文を生成すると、太字## 小見出し---のようなマークダウンのトークンがHTMLに変換されないまま残ってしまうことがよくありました。本番環境でアスタリスクがそのまま表示されてしまっていたのです。

2つ目は、作成直後は問題ないものの、公開直前のどこかのフック(hook)がコンテンツを台無しにしてしまうケース。特定の関数が本文内で

を余分に開いたまま閉じ忘れたためにカードやサイドバーのレイアウトが崩れたり、価格表の自動挿入が空の

動作原理

検問所は2つのステップで構成されています。

ステップ1:sanitize(サニタイズ) — 無条件で修正する作業

HTMLを受け取り、以下を一律で適用します。

  • 危険なインラインスタイルの除去(width:800pxmargin-left:-30pxposition:absoluteなど)
  • ブログ記事公開直前の自動QC — 6ヶ月間誤公開0件の秘密タグの固定width/height属性の除去 → レスポンシブ対応の維持
  • マークダウンの残りトークンをHTMLに変換(XX、任意の---
  • 絵文字ポリシー違反文字の除去(U+2600〜27BF、U+1F000〜1FAFFの範囲)
  • ボックススタイル(borderbox-shadowpadding>20pxが指定された
    )のフラット化
  • 本文コンテナへの安全なCSSの1行強制挿入(max-width:100%overflow-wrap:anywhere

このステップは、人の判断を必要としない機械的な作業です。どのような記事であっても、同じ結果が得られるようにしました。

ステップ2:quality gate(クオリティゲート) — パス失敗時に公開をブロック

人が見れば気づくような抜け漏れを自動でチェックします。パスできない場合、公開自体が拒否されます。

  • 本文の文字数が600文字未満 → fail
  • が3個未満 → fail(ガイド・比較記事)

  • 画像が0枚 → fail(記事のタイプを問わず)
  • 実際の効果

    導入後6ヶ月間の結果:

    • マークダウンの残り露出トラブル:導入前は平均月4件 → 導入後は0件
    • 左右の見切れ:平均月7件 → 0件
    • 空の表 / 空のグラフ本文:平均月3件 → 0件
    • 公開がブロックされた記事:累計38本(すべて作成者が修正した後に再公開)

    ブロックされた38本は、記事が失われたわけではありません。作成者が事前に気づいてブラッシュアップした上で再試行したため、すべて正常に公開されました。ブロックされた理由の分布は、出典漏れ 41% / 文字数不足 26% / 画像0枚 21% / その他 12% でした。

    検証方法

    検問所を作成した後に、私たちが行った検証です。

    ゴールデンセット回帰テスト — 過去にトラブルが発生した記事41本の原文を集めてゴールデンセットを作成しました。sanitize + quality gate を通過させた際、41本すべてでトラブルのパターンが消えるかどうかを自動比較しました。結果は39/41がパス。失敗した2本を確認して正規表現を補強し、41/41パスまで引き上げました。

    本番環境のスポットチェック(spot-check) — 新しい sanitize を適用した最初の週に公開された18本のうち、8本をランダムに選んで本番ページを直接フェッチ(fetch)しました。デスクトップ(desktop)1280px、モバイル(mobile)360pxの2つの幅で横スクロールが発生していないか、文字がコンテナの外にはみ出していないか、画像が崩れていないかをチェックしました。結果は8/8で正常でした。

    ダブルパスの冪等性(idempotency) — sanitize を一度通過した出力結果に対して、もう一度 sanitize を通過させた際に出力が同一になるかを確認しました。これは、公開フックチェーン(publish hook chain)が2回実行されるケースを防ぐための検証です。結果は100/100で同一でした。

    実装方法

    コード全体をそのままコピーするよりも、コアとなる1つか2つの要素をご自身の環境に合わせて取り入れることをおすすめします。

    
    import re
    
    def sanitize_pre_publish(html: str) -> tuple[str, list[str]]:
     fixes = []
     # 危険なインラインwidthの除去
     html, n = re.subn(r'width\s*:\s*(?:[4-9]\d{2}|[1-9]\d{3,})px\s*;?', '', html)
     if n: fixes.append('strip_wide_width')
     # マークダウンの残り → HTML
     html, n = re.subn(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', html)
     if n: fixes.append('md_bold')
     # 絵文字の除去(必要に応じて)
     html, n = re.subn(r'[\U0001F300-\U0001FAFF]', '', html)
     if n: fixes.append('strip_emoji')
     return html, fixes
    
    def quality_gate(html: str, post_type: str) -> tuple[bool, list[str]]:
     fails = []
     text = re.sub(r'<[^>]+>', '', html)
     if len(text.replace(' ', '')) < 600: fails.append('too_short')
     if html.count('<h2') < 3 and post_type in ('howto', 'compare'): fails.append('few_h2')
     if '<img' not in html: fails.append('no_image')
     if 'TODO' in html or 'REDACTED' in html: fails.append('placeholder')
     return (len(fails) == 0), fails
    

    この2つの関数を、公開直前のただ1箇所で呼び出すだけで十分です。quality_gate が fail を返した場合は公開をブロックし、その理由を作成者にフィードバックしてください。sanitize は、出力されたHTMLを受け取ってそのまま公開APIに渡します。

    要約すると、「公開前の検問所1箇所で、すべてのトラブルを自動的に防ぐ」という1行に尽きます。人が毎回検品に費やしていた時間が丸ごと不要になります。

    Category Coverage Notice

    This article follows our label-specific editorial criteria. Details:

ToolSignal Pro Editorial

ToolSignal Pro는 AI·IT·소프트웨어 트렌드를 다루는 종합 IT 인사이트 매거진입니다.

이전 글 다음 글