検索の管理画面を開くと、「インデックスに登録されていない理由」の欄に「ページにリダイレクトがあります」という項目が並んでいました。件数のところに数字が付いていると、どうしても直さなければいけないものに見えてしまいます。
私自身、自分の Web サイトでこの通知を見たとき、最初は転送を全部洗い出して消そうと考えました。ところが、一覧の URL を辿ってみると、私が意図して置いた言語判定の転送が含まれており、これは対応不要と判断できました。
ここでは、転送を「対応不要」「束ねる」「直す」に分けた基準と、その仕分けを自動で行ったスクリプトを書き残します。エージェントに調査を頼む場合の指示文も添えました。
先にお伝えしたいのは、通知の意味です
この項目が示しているのは、「その URL は転送元なので登録の対象にならず、転送先のほうが対象になる」という状態です。転送そのものが悪いという意味ではありません。
だからこそ、見るべきは転送元ではなく、転送先と、そこへ着くまでの道のりです。私は次の表で仕分けました。
| 転送の種類 | 判断 | 理由 |
|---|---|---|
| http から https への統一 | 対応不要 | 転送元が残るのは意図どおりです |
| www の有無の統一 | 対応不要 | どちらか一方に寄せるための転送です |
| 末尾スラッシュの統一 | 対応不要 | 同じ中身への別表記を一本化しています |
| 言語による振り分け | 対応不要(ただし 302/307 か確認) | 利用者ごとに行き先が変わるため、一時的な転送のほうが安全です |
| 2 回以上の連続転送 | 1 回に束ねる | 経由のたびに到達が遅れ、途中の設定変更で壊れやすくなります |
| ループ | 直す | どこにも着けません |
| 転送先が 404 や 5xx | 直す | 転送元の価値を捨てています |
| 転送先が noindex | 意図を確認 | わざとなら正しく、うっかりなら登録の機会を逃します |
大事なのは、見た目の件数ではなく、この表のどの行に当たるのかを URL ごとに言えることだと考えています。
一覧を一つずつ辿る仕分けスクリプト
管理画面から書き出した URL の一覧を渡すと、転送を自動追従せずに一歩ずつ辿り、上の表の判断を付けて返すスクリプトです。標準ライブラリだけで動きます。
ポイントは二つあります。一つめは、urllib の自動追従を止めて、3xx をそのまま受け取ることです。自動追従に任せると、途中の経由地がすべて見えなくなります。二つめは、同じ URL を素の要求と Accept-Language: en 付きの要求の二通りで辿ることです。理由は、このあと表で説明します。
#!/usr/bin/env python3
"""redirect_triage.py — Search Console の「ページにリダイレクトがあります」を仕分ける。
使い方: python3 redirect_triage.py urls.txt (1 行 1 URL。管理画面から書き出した一覧)
標準ライブラリのみ。自動追従を止め、1 ホップずつ自分で辿る。"""
import sys, re, urllib.request, urllib.error, urllib.parse
class NoFollow(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *a, **k): # 追従しない = 3xx をそのまま例外で受ける
return None
OPENER = urllib.request.build_opener(NoFollow)
def fetch(url, lang=None):
"""(status, location, headers, body先頭) を返す。ネットワーク失敗は status=0。"""
req = urllib.request.Request(url, headers={"User-Agent": "redirect-triage/1.0"})
if lang:
req.add_header("Accept-Language", lang)
try:
with OPENER.open(req, timeout=10) as r:
return r.status, None, r.headers, r.read(4096).decode("utf-8", "ignore")
except urllib.error.HTTPError as e:
return e.code, e.headers.get("Location"), e.headers, ""
except Exception:
return 0, None, {}, ""
def chain(url, lang=None, limit=6):
hops, seen = [], set()
while len(hops) < limit:
if url in seen:
return hops, "loop"
seen.add(url)
st, loc, hd, body = fetch(url, lang)
hops.append((url, st))
if st in (301, 302, 303, 307, 308) and loc:
url = urllib.parse.urljoin(url, loc)
continue
noindex = "noindex" in (hd.get("X-Robots-Tag", "") if hd else "").lower() \
or re.search(r'<meta[^>]+name=["\']robots["\'][^>]+noindex', body, re.I)
return hops, ("noindex" if noindex and st == 200 else st)
return hops, "too-long"
def classify(src, hops, end):
if end == "loop": return "NG", "ループしています"
if end == "too-long": return "NG", "転送が長すぎます"
if isinstance(end, int) and (end >= 400 or end == 0):
return "NG", f"転送先が {end} を返しています"
if end == "noindex": return "CHECK", "転送先が noindex です(意図したものか確認)"
if len(hops) >= 3: return "FIX", f"{len(hops)-1} 回転送されます(1 回に束ねる)"
if len(hops) == 1: return "OK", "転送なし"
a, b = urllib.parse.urlsplit(src), urllib.parse.urlsplit(hops[-1][0])
if a.scheme != b.scheme and (a.netloc, a.path) == (b.netloc, b.path):
return "OK", "http→https の統一"
if a.netloc.replace("www.", "") == b.netloc.replace("www.", "") and a.netloc != b.netloc:
return "OK", "www の有無の統一"
if a.path.rstrip("/") == b.path.rstrip("/") and a.path != b.path:
return "OK", "末尾スラッシュの統一"
return "CHECK", f"別の URL へ転送: {b.path}"
def main(path):
for src in [l.strip() for l in open(path) if l.strip()]:
hops, end = chain(src) # 検索ロボットに近い素の要求
hops_en, end_en = chain(src, lang="en") # 英語ブラウザ相当
level, note = classify(src, hops, end)
if level == "OK" and (hops[-1][0] != hops_en[-1][0]):
first = hops_en[0][1]
level = "OK" if first in (302, 303, 307) else "FIX"
note = "言語による振り分け" + ("(一時転送・想定内)" if level == "OK"
else f"({first} は端末に覚えられるので 302/307 へ)")
elif len(hops) == 1 and len(hops_en) > 1:
first = hops_en[0][1]
level = "OK" if first in (302, 303, 307) else "FIX"
note = "言語による振り分け" + ("(素の要求は転送なし・想定内)" if level == "OK"
else f"({first} は端末に覚えられるので 302/307 へ)")
print(f"{level:5} {src}\n -> {note}")
if __name__ == "__main__":
main(sys.argv[1])出力の先頭に付く「OK」「CHECK」「FIX」「NG」は、それぞれ対応不要、意図を確認、束ねる、直す、に対応しています。