前提: 公開ルートの漏れを機械検査していた

以前、公開ページがsitemapに載っているか(あるいは載せない理由が明記されているか)を機械的に検査するテストを作りました。カスタムモジュールのrouting.ymlを静的解析し、パラメータを持たない公開GETルートをすべて集めて、sitemap掲載か除外リスト登録のどちらかを要求します。

Drupalを起動せずソースの静的解析だけで完結するため、DBもブートも不要で高速に回ります。

症状: 2件、本番で公開され続けていた

全ページを機械走査したところ、このテストが検出していなかった問題が2件見つかりました。

1件目。 /nodeが200・index, followで公開されていました。中身はニュース記事の一覧で、既存の/newsと内容が重複しています。しかもtitleもH1も空文字でした。

2件目。 その/news自体がsitemapに載っていませんでした。index, followを返す公開ハブで、全ページから348本の被リンクを持つにもかかわらずです。

どちらも、このテストが緑(成功)のまま起きていました。

真因: routing.ymlしか見ていなかった

/nodeと/newsはどちらもDrupal標準のfrontpageビュー(View)が提供するページです。Viewsが作るルートはrouting.ymlには現れません。 Viewsは実行時にconfig(views.view.*.yml)からルートを動的に生成する仕組みで、静的なYAMLファイルとして事前に存在しないからです。

つまりテストの収集ロジックは、最初からViews由来のページを見ることができない構造になっていました。

// 是正前: config/sync/*.routing.yml しか走査していなかった
foreach (glob($this->repoRoot() . '/web/modules/custom/*/*.routing.yml') ?: [] as $file) {
  // ...
}

「検査を通している」ことと「検査対象になっている」ことは別でした。

是正: Viewsのdisplayも走査対象に加える

config/sync/views.view.*.ymlから、pathを持つ有効なdisplayを抽出する検査を新設しました。

private function collectViewPagePaths(): array {
  $paths = [];
  foreach (glob($this->repoRoot() . '/config/sync/views.view.*.yml') ?: [] as $file) {
    $view = Yaml::parseFile($file);
    if (!is_array($view) || ($view['status'] ?? FALSE) !== TRUE) {
      continue;
    }
    foreach ($view['display'] ?? [] as $display_id => $display) {
      $options = $display['display_options'] ?? [];
      $path = $options['path'] ?? NULL;
      if (!is_string($path) || $path === '') {
        continue;
      }
      // 明示的に無効化されたdisplayはルートを持たない。
      if (($options['enabled'] ?? TRUE) === FALSE) {
        continue;
      }
      if (str_contains($path, '%') || str_starts_with($path, 'admin')) {
        continue;
      }
      $paths['/' . ltrim($path, '/')] = ($view['id'] ?? '?') . '.' . $display_id;
    }
  }
  return $paths;
}

引数プレースホルダ(%)を含むdisplayは個別URL単位でsitemap掲載が決まるため対象外にし、管理画面配下も除外します。残ったものは、sitemapに載っているか、理由つきの除外リストに入っているかのどちらかを要求します。

この検査を実際に走らせると、/newsが両方の条件を満たさず検出されました。sitemapへ追加し、/nodeは使われていないページだったため、display自体を無効化しました(実際に404になることを確認済みです)。

検証: 検査自体が空振りしていないかを確かめる

検査を追加しただけでは、その検査が本当に機能しているかは分かりません。意図的に検出対象を作り、検査がそれを本当に拾うかを確かめました。

/newsをsitemapの登録から一時的に外し、新設した検査を走らせたところ、想定どおり違反として報告されました。検査を通すための最短の道は、検査そのものを壊すことでもあります。 一度検査を無効化してみて、ちゃんと失敗することを確認するまでは、検査が効いているとは言い切れません。

まとめ

  • 公開ページの漏れを検査するとき、ルートの出どころが1種類だけとは限りません。 routing.ymlの静的解析だけでは、実行時にルートを動的生成する仕組み(Views等)を取りこぼします
  • 「検査を通っている」は「検査対象になっている」ことを意味しません。収集ロジックがそもそも見ていない領域には、どんな不正も無条件で通過します
  • 検査を新設したら、意図的に検出対象を壊してみて、検査が本当に拾うかを確認してください。 検査が緑であることと、検査が機能していることは別の確認です
  • 同じ内容のページが2つのURLで公開されていると、どちらが正規かをクローラーが判断できません。titleやH1が空のページはその兆候として現れやすいので、機械走査の対象に含める価値があります