静的サイトのcanonical自己参照とhreflang相互性をPythonで機械検査する

ページ数の多い静的サイトを日英ミラーで運用していると、canonical が別ページを指していたり、hreflang の対応先が片方向だけだったりする崩れが、レビューをすり抜けて本番に出てしまう。こうした崩れは Google の重複判定やインデックス漏れの原因になる。

自分が運用している「ハシトシステム」(hashitosystem.com)は、日本語ページと /en/ 配下の英語ミラーを持つ 200 ページ超の静的サイトだ。ここでは、デプロイ前に必ず通すオンページSEOの機械検査を、依存ゼロの Python で最小構成に落として手元で動かす。

何を検査すべきか

重複・正規化まわりで壊れやすいのは次の4点だ。

  1. indexable なページの canonical が自己参照になっているか(別URLを指していないか・欠落していないか)
  2. canonical / title / meta description が indexable 間で重複していないか
  3. hreflang の ja / en が相互に一致しているか(対応ページが片方向で欠けていないか)
  4. indexable なのに hreflang を持たない「孤立ページ」がないか(作業用・デモは noindex にして除外する)

ポイントは、<meta name="robots" content="noindex"> のページは監査対象から外すこと。意図的に検索対象外にしたページまで「hreflang が無い」と怒られては運用が回らない。

最小の監査スクリプト

public/ 配下の HTML を走査し、上記を検査する最小版が以下だ。正規表現で必要なタグだけ拾う。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# seo_audit_min.py — public/ 配下の canonical/title/description/hreflang を検査
import os, re, html, glob

BASE = "https://example.com" # 自サイトの正規オリジンに置き換える

def tag(h, pat):
m = re.search(pat, h, re.I)
return m.group(1) if m else None

def canon(h): return tag(h, r'<link rel="canonical" href="([^"]+)"')
def title(h): return html.unescape(tag(h, r'<title>(.*?)</title>') or "")
def desc(h): return html.unescape(tag(h, r'<meta name="description" content="([^"]*)"') or "")
def hl(h, w): return tag(h, r'<link rel="alternate" hreflang="%s" href="([^"]+)"' % w)

def expected_url(path):
# public/tools/x/index.html -> https://example.com/tools/x/
rel = os.path.relpath(path, "public").replace(os.sep, "/")
rel = re.sub(r'index\.html$', '', rel)
return BASE + "/" + rel

problems, titles, descs, canons = [], {}, {}, {}
files = glob.glob("public/**/*.html", recursive=True)

for f in files:
h = open(f, encoding="utf-8").read()
if 'content="noindex' in h:
continue # noindex は監査対象外
exp = expected_url(f)
c = canon(h)
if not c:
problems.append((f, "canonical-missing"))
elif c != exp:
problems.append((f, f"canonical-not-self ({c} != {exp})"))
titles.setdefault(title(h), []).append(f)
descs.setdefault(desc(h), []).append(f)
canons.setdefault(c, []).append(f)
ja, en, xd = hl(h, "ja"), hl(h, "en"), hl(h, "x-default")
if not (ja and en):
problems.append((f, "indexable-no-hreflang (sample は noindex に)"))
elif xd and xd != ja:
problems.append((f, f"x-default-not-ja ({xd})"))

def dup(m, label):
for k, fs in m.items():
if k and len(fs) > 1:
problems.append((fs[0], f"{label}: {len(fs)} pages share '{str(k)[:40]}'"))

dup(titles, "duplicate-title")
dup(descs, "duplicate-description")
dup(canons, "duplicate-canonical")

print("indexable pages checked:", sum(1 for f in files
if 'content="noindex' not in open(f, encoding="utf-8").read()))
for f, p in problems:
print("NG", os.path.relpath(f, "public"), "-", p)
print("OK" if not problems else f"{len(problems)} problem(s)")

手元で試すには、public/ に日英2ページを置いて走らせる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mkdir -p public/tools/demo public/en/tools/demo
cat > public/tools/demo/index.html <<'HTML'
<link rel="canonical" href="https://example.com/tools/demo/">
<link rel="alternate" hreflang="ja" href="https://example.com/tools/demo/">
<link rel="alternate" hreflang="en" href="https://example.com/en/tools/demo/">
<link rel="alternate" hreflang="x-default" href="https://example.com/tools/demo/">
<title>デモ</title><meta name="description" content="日本語デモ">
HTML
# 英語版はわざと canonical を日本語版に向けて壊してみる
cat > public/en/tools/demo/index.html <<'HTML'
<link rel="canonical" href="https://example.com/tools/demo/">
<link rel="alternate" hreflang="ja" href="https://example.com/tools/demo/">
<link rel="alternate" hreflang="en" href="https://example.com/en/tools/demo/">
<link rel="alternate" hreflang="x-default" href="https://example.com/tools/demo/">
<title>Demo</title><meta name="description" content="English demo">
HTML
python3 seo_audit_min.py

英語版の canonical が自己参照(/en/tools/demo/)でないため、canonical-not-self として検出される。正しく /en/tools/demo/ に直すと、indexable pages checked: 2OK になる。

運用に組み込む

この検査をデプロイ前の必須ゲートにすると、崩れが本番に出なくなる。実運用では、

  • title / description はページ固有にして重複ゼロを保つ
  • hreflang は ja / en / x-default を必ず相互に張り、x-default は ja に合わせる
  • 検索に載せたくない作業用ページは noindex, follow を付けて sitemap からも外す

というルールをスクリプトで機械的に守らせる。手作業のチェックリストは必ず抜けるので、「OK が出るまでデプロイしない」という一点に集約するのがコツだ。

実際にこの監査を通して運用しているツール群は hashitosystem.com で公開している。QRコード生成やダミーデータ生成など単機能ツールを日英ミラーで並べており、各ページの canonical / hreflang はこの検査を通したうえで配信している。


静的サイトのcanonical自己参照とhreflang相互性をPythonで機械検査する
https://blog.hashito.biz/2026/07/12/hashitosystem-seo-audit-canonical-hreflang-static-site/
著者
hashito
作成日
2026年7月12日
著作権