# Distan

> WordPress を制作環境として使い、納品用の静的 HTML を書き出すプラグイン。本番サーバーで動かすものではなく、成果物（静的 HTML）を作るための道具。HTML 納品案件（採用サイト、キャンペーンサイト等）向け。

Distan は公開中の全ページをループバック HTTP で巡回し、静的 HTML として書き出す。`get_header()`・テンプレート階層・カスタムフィールド・条件分岐はすべて通常どおり評価され、その結果が焼き込まれる。内部リンクはドキュメント相対または本番 URL 絶対に変換され、ZIP 納品や FTP 上書きに最適化されている。制作は WordPress、納品は静的 HTML、という分離により、本番から動的な攻撃対象（PHP・DB・WordPress 一式）を無くせる。

## ドキュメント

- [README.md](README.md): 人間向けの機能説明・使い方・フィルタ一覧
- [ai-reference.md](ai-reference.md): AI 向けの「できること／できないこと」リファレンス
- [CLAUDE.md](CLAUDE.md): コードを編集する開発者・AI 向けの設計ドキュメント
- [DESIGN.md](DESIGN.md): 管理画面のビジュアルデザイン仕様（Google の design.md 形式）
- [TESTING.md](TESTING.md): 変更後の検証手順

## 主な機能

- 静的 HTML 書き出し（フロントページ／全シングル／アーカイブ＋ページネーション／404）
- URL 変換（ドキュメント相対 or 本番 URL 絶対）と本番 URL への置換（`distan_url_replacements` で複数指定可）
- 構造化データ（JSON-LD）の保持と URL 置換。schema 設定（既定オフ）で、既存 JSON-LD のない静的ページに基本 WebPage を追加。公開URLが必須。実出力パス・HTMLのタイトル/説明/言語を使用。404除外。schema_exclude_dirs（配下含む）/schema_exclude_pages（完全一致）にサイト相対の元パスを1行ずつ指定。既存JSON-LDは削除しない。
- クリーン HTML（WordPress の痕跡除去、noindex 除去）
- 構造化データ診断: 自動追加オフでもJSON-LDを読み取り検査。空/不正JSON、開発URL、ページ型URLとcanonical、同一ページ内@id定義を確認。参照だけの@idは警告しない。manifest.schema_auditを管理画面と各納品レポートで使用。解析上限・生成失敗は未検査として記録。語彙/意味/Google適格性の完全検証ではない。外部通信・自動修正なし。
- HTMLコメント削除（strip_comments・既定オフ）: 納品HTMLからプレーンな`<!-- -->`を除去。apply_page_markers後・書き出し直前に適用（distan:マーカーと競合しない）。script/style/textarea/pre内は保護、IE条件付きコメント（<!--[if）は残す。PCRE失敗時は無変更で返す
- パス平坦化（テーマ→assets/、uploads→media/）
- キャッシュバスティングのクエリ保持（ファイル名不変で FTP 上書き可）
- 差分レポート・自動掃除・差分 Markdown・ZIP 書き出し（全体）／差分 ZIP 書き出し（前回から変わった分のみ・本番と同じ相対パス・DELETE.txt と distan-diff.md 同梱）
- 大きいファイルの検出と警告（バイナリはストリームコピー、一定サイズ超はレポートに一覧。既定 10MB、distan_large_file_threshold で変更可。コピーはするので手作業不要）
- Markdown 書き出し（選択制、content.md、Gemini Notebook（旧NotebookLM）等の AI ツール向け）
  - 絞り込み（Distan_Markdown::wants）: 何も指定しなければ全ページ（投稿・CPT・固定ページ・トップ・アーカイブ＝1.7同等 full export）。フィルタ設定時のみ絞り込み——投稿タイプ（md_post_types・チェックしたタイプだけ）／公開日（md_date_from〜md_date_to・両端含む）で限定し、絞込中はトップ/アーカイブ/固定ページが外れる。個別の固定ページ（md_pages・検索＋セレクト＋「追加」でチップ選択・テンプレート書き出しと同UI）は絞込に関係なく常に採用。静的サイト（dist/）は不変（content.md 側だけのゲート）
  - Markdown書き出し: 表→パイプ表・pre/code→フェンス/バッククォート・img→![alt]・blockquote→>・strong/em→**/*・ol→番号・hr→---。各セクションにtitle/URL/description。titleは<title>→本文h1→URLの順でフォールバック、本文先頭の重複見出しは除去。TOCは逐次書き出し設計と衝突のため未実装
- サイトマップ書き出し（選択制、sitemap.xml、Google Search Console 対応。著者・日付アーカイブは載せないので ID 露出なし。スラッグ以下や語を含む URL を除外可）
- robots.txt 書き出し（選択制、最小構成。サイトマップ有効時は Sitemap: 行を記載）
- リンク監査
- 列挙の由来（provenance）記録。差分レポートは「投稿タイトル [投稿 #123]」で変更を名指す（増分スキップはしない）
- カスタム URL ソース（distan_sources）で、列挙が発見できない URL を第一級で追加（重複排除・差分の対象。distan_collect は生の最終手段）
- URL パラメータでの出し分けに対応（選択制）。distan_variant_keys で表示を分けるクエリキー（例 tab, lang）を宣言すると、/slug/?tab=a と ?tab=b が独立した静的ファイル（/slug/tab-a/index.html 等）になり内部リンクも畳み込み先へ書き換わる。宣言キー以外のクエリ（utm_* 等）は無視。畳み込み形式は distan_query_variant_segment で変更可。既定オフ
- コアサイトマップ突き合わせ（wp-sitemap をプロセス内で読み、未生成 URL をレポートに一覧。distan_use_core_sitemap で補助ソースとして列挙にも合流可、既定オフ）
- 差分エクスポート（変更分のみ）。前回生成からの追加・変更ファイルだけを ZIP 化（本番と同じ相対パスで解凍して上書き可）。削除対象は DELETE.txt、概要は distan-diff.md。変更検知はページ HTML の内容ハッシュ（パス不変でも中身変化を検出、アセットは追加/削除で追跡）。全体 ZIP は初回・節目用に残置。差分の基準は distan_manifest_source で db（既定・単一環境）/ output（成果物同梱の携行 manifest .distan/manifest.json、生成とデプロイが別環境のとき）を選択。既定 db で挙動は不変
- 取りこぼしの取り込み。列挙は DB を読むのでプラグインが動的登録する URL（フォーム完了画面・仮想ルート）を知らない。コア sitemap 照合（sitemap_missing）やリンク切れ（broken）で存在は見えるが、生成に足すには従来 distan_sources を手書きする必要があった。生成画面で候補（コア sitemap にあって未生成の URL）を「含める／今後表示しない／未決」から URL ごとに選ぶ（Distan_Takeup、オプション distan_takeup、include/ignore の 2 リスト）。既定オプトインで選んだものだけが collect() で distan_sources と同経路（make_item→dedup、origin=takeup）でキュー投入。判断は記憶。ignore は include より優先で、書き出したくない URL は混ざらない。コア sitemap 外の URL は自由入力欄で追加（同一オリジンのみ）。検知は既存（missing_from / audit_links）、追加したのは取り込みアクション
- テンプレート書き出し。生成済みページを 1 枚選ぶと、そのページ＋参照アセットのみ（CSS・JS・フォント・画像、url()・@import も再帰追従）を本番と同じ相対パスで ZIP 化（Distan_Report::build_template_zip）。共通ヘッダー・フッターに沿った特設ページ制作を外部委託する際の雛形。全ページ分の素材は同梱せず、ナビ遷移先ページは 1 枚納品では欠落する前提。ZIP ルートに制作者向け README.md 同梱（共通CSS/JS・header/footer・相対パスを維持。本文とページ固有のtitle/description/canonical/OGP/JSON-LDは転用先に合わせて更新）。本文領域の切り出しはしない。生成画面の「テンプレート書き出し」設定（既定オン）で表示切替
- プラグイン一覧に「設定」リンク（plugin_action_links → admin.php?page=distan）
- テンプレート候補のライブ絞り込み。全候補を Alpine x-data に JSON で渡し x-for で option をタイトル部分一致フィルタ。生成経路に非依存・フロント完結
  - テンプレートマーカー（通常生成・テンプレート書き出し両方で有効）: `<!-- distan:no-block-styles -->`＝ブロックCSS除去(wp-block-library/-theme link＋id が wp-block-*/global-styles* のインラインstyle全部＝library-inline/per-block/placeholder。wp-img-auto-sizesも対象)、`<!-- distan:drop-assets <prefix>... -->`＝出力相対パス前方一致でscript/style除去（plugin/coreは wp-content/plugins/・wp-includes/ のまま）。トップレベルlink/scriptのみ・残CSSのurl()は不干渉・タグごと除去・マーカーも除去・推定なし。通常生成でも apply_page_markers を書き出し前に適用（該当link/script＋inline除去、マーカーも除去）。共有ファイル実体は消さずページの参照タグだけ外す＝他ページ無傷
- 生成完了フック（distan_after_generate）で自動デプロイに接続（Distan自体はデプロイしない）
- デプロイフック（distan_dispatch）は目視確認後の手動ボタンで発火する人間のゲート。承認状態は持たず最終デプロイ時刻のみ記録。既定オフ

## できないこと

- 本番での動的処理（フォーム・検索・コメント等）は動かない
- 商品・評価・FAQ・組織などの構造化データは推測しない（任意の基本 WebPage 追加のみ）
- リアルタイム更新はしない（外部データは生成時点で凍結、更新は再生成）
- ES modules は file:// で動かない（クラシックテーマなら回避可能）

## 開発URL診断の共通化（1.9.1）

Distan_Url_Auditがホスト・ポート・パス範囲の判定を共通化。生成器は保存ファイルの検査用コピーでスラッシュ/Unicodeエスケープを読み取り、JSON-LD診断はデコード済みの値を同じ判定へ渡す。公開元と同じ公開先は警告しない。既定ポートを正規化し、別ポート・似た別ホスト・パス境界を区別する。全体はURL出現数、JSON-LDは問題のページ/ブロック単位であり集計単位は異なる。HTMLの自動修正・外部通信はしない。


## 失敗・中断時の配布と差分（1.9.2）

生成開始前にmanifest.complete=falseを保存し、直前の成功記録のfiles/hashes/entriesをdiff_baselineに保持する。ジョブ消失・途中停止でも配布を再開しない。失敗時のadded/modified/removedは未確定として空、errorsを保持。次回成功時の差分は成功記録から計算する。DB/output両方式、旧manifest（completeなし）との互換性を保つ。既に失われた旧版の成功記録は復元しない。

全体・差分・テンプレートZIPと手動dispatchは未完了のmanifestを拒否。生成完了フックは記録されたエラーなしの場合だけ発火する（1.9.1以前からの変更）。ページ取得・保存に加えて、キューに入ったアセットの読み取り/コピー失敗もerrorsに入れ、CSSで発見した参照アセットをmanifestと掃除の保持対象に含める。警告やリンク切れは生成失敗とは別。ファイルは逐次上書きで、ロールバックではない。同時実行の厳密な排他や生成中の設定変更は本修正の範囲外。

回帰検証: `php tests/generation-failure.php`（GitHub版のみ）。LocalでのPlugin Check・画面確認は別途行う。
