非wwwへの301リダイレクトをサイトルートの.htaccessに足したのに、記事フォルダ配下だけが200のまま重複配信され続けていた。原因はApacheの仕様——自前の.htaccessを持つ子ディレクトリは、サイトルートのrewriteルールを継承しない。3つのメディアをフォルダで分けて運用している自サイトで、実際に踏んだ落とし穴の記録。
「ルートに1回書けば全体に効く」という直感が崩れる具体的な条件と、直したはずの修正が実は1段も効いていなかったことに、本番URLを1本ずつ叩いて気づいた検証の型。サイトマップのlastmodが1件も入っていなかった副次発見もあわせて。
Search Consoleのカバレッジレポートで、「検出-インデックス未登録」の件数が1週間で33件から45件へ悪化していた。同時に「代替ページ(canonicalあり)」も35件から44件へ増えている。サイトマップに載っている217 URLをGooglebotのUser-Agentで全件たたくと217/217とも200を返していて、公開されているページ自体は壊れていない。
つまり、ページは生きているのにインデックス登録側の数字だけが悪化していた。原因を追うところから始まった。
本番URLを1本ずつ実測すると、未修正の障害が2つ見つかった。
ブログの1メディア(AIコーディングTips)で、/ArtN/形式のURLが全件403を返していた。1週間前に別メディアの重複URL修正をした際、同じ301ルールを書いた.htaccessがこのメディアのフォルダには存在していなかった——単純な作業漏れだった。
非www側(https://meclgg.com/...)が301せず200を返し続けていた。canonicalはwww側を指しているので理屈上は「代替ページ」として除外されるはずだが、Googleは除外する前に一度クロールしてから捨てている。200本超の記事×2ホスト分をクロールしていることになり、これが「検出-インデックス未登録」を直接圧迫していた。
ここが要点。ページ自体は正常でも、同じ内容を返すホストが2つ存在するだけでクロール予算は倍かかる。しかもcanonicalで正規化していれば「順位に悪影響はない」と思い込みがちだが、クロールされる回数そのものは減らない。
サイトルートの.htaccessに「非www→wwwへの301」を追加し、本番でhttps://meclgg.com/や/about/などが正しくwwwへリダイレクトされることを確認した。ここまでは想定どおりだった。
ところが、https://meclgg.com/wellbeing/・/aitips/・/app/——つまり記事フォルダの配下だけが、いつまで経っても200のままだった。ルートは直っているのに、その一段下だけ直っていない。
調べると原因はApacheの仕様だった。自前の.htaccessにrewriteディレクティブを持つディレクトリは、サイトルートのrewriteルールを継承しない。「マージされて両方効く」のではなく、そのディレクトリ配下では、一番深い階層にある.htaccessのルールが親のルールをまるごと置き換える形で動く。
本番を走査すると、rewriteルールを持つ子ディレクトリが3つあった。1つは前段の修正で301群を書いた記事フォルダ、もう1つは今回追加したばかりの記事フォルダ、もう1つはRewriteEngine Offだけを書いてルート側のWordPress関連ルールを避けていたフォルダ。どれもルートの非www→www 301を継承していないという点では共通していた。
つまり、サイト全体の記事(3メディア合計で200本超)が、ルートの修正を1本も受け取れないまま非www側でも配信され続けていたことになる。「直したはず」が、実際には一番効かせたかった部分に限って効いていなかった。
ルートを継承させる方法はないので、同じルールを3つの子.htaccessそれぞれの先頭に複製する方針にした。Apacheのrewriteルールは%{REQUEST_URI}のようなサーバ変数を使う限り、サブディレクトリの文脈でもサイトルートからの完全パスとして評価されるため、ルートに書いたのと同じ記述がそのまま子ディレクトリでも機能する。
「先頭」に置くのが2つ目のポイントだった。各ディレクトリには元々、ディレクトリ形式URLをarticle.htmlへ301するルールがある。ホスト正規化をその前に置くことで、リダイレクトが「非www・ディレクトリ形式」→「www・article.html」の1ホップで済む。後ろに置くと、まず旧ホストのままarticle.htmlへ飛び、そこから改めてwwwへ、という2ホップになってしまう。
RewriteEngine Offで隔離していたフォルダも、正規化ルール1本だけを持つRewriteEngine Onに置き換えた。子コンテキストはそもそもルート側の他のルール(WordPress関連の受け皿ルール)を継承しないので、この1本を足しても意図していた隔離は崩れない。
反映は3ファイルを対象にするため、バックアップ取得とアップロード後のバイト照合をセットにした専用スクリプトを新設して行う設計にした。.htaccessは手で1文字直すと壊れたときの被害が大きいファイルなので、差分適用ではなく「アップロード後に取得し直して元ファイルとバイト単位で突き合わせる」検証を必須にしている。
この調査の途中で、サイトマップ生成側にも見落としが見つかった。217件あるURLエントリのうち、更新日時を示す<lastmod>が1件も出力されていなかった。代わりに入っていたのは更新頻度のヒント<changefreq>だけだったが、Googleはクロール順の判断材料としてchangefreqではなくlastmodを使う。200本を超える記事が、優先順位の手がかりなしに並んでいたことになる。
各記事が持っている投稿日をISO形式に変換して出力する処理を足し、あわせてカテゴリのトップページには配下記事の最新更新日を継承させた。再生成後、217件のうち212件にlastmodが入り、URLの集合自体は修正前と完全に一致することも確認した。
ここまでの一連の修正のうち、ホスト正規化を子.htaccessの先頭へ複製する最後の1本は、この記事を書いている時点でまだ本番への反映待ちになっている。.htaccess3ファイルへの書き込みは、外部への公開操作としてこのサイトの承認フローを通す対象にしているためだ。
数日後にあらためてサイトマップとインデックス状況の差分を取ったところ、直近1〜2週間に公開したばかりのページを除いても、まだ数十件規模の差分が残っていた。優先度の高いページを個別に本番実測すると、HTTP 200・サイトマップ掲載・noindexなし・自己canonicalのいずれも問題なく、技術的に明白な障害はもう見当たらない——つまり残る原因は、今回のようなサーバ設定の不具合ではなく、クロール順やページ単位の評価に移っている可能性が高い。公開から14日を境に「まだ新しいから待つべきもの」と「恒常的な問題」を切り分けながら追跡している最中で、この記事に結論めいたオチはまだない。
メディアごとにフォルダを分けて運用している、共有ホスティング上のApacheサイトなら、同じ構造で同じ落とし穴を踏む可能性がある。
.htaccessを持ち、かつrewriteディレクティブを含む場合、そのディレクトリは親のルールを継承しない。マージではなく置き換えで動く。lastmodが本体、changefreqは補助情報クロール順のヒントとして機能するのは更新日時の実データであって、更新頻度の申告ではない。