2026-09-05 · html · seo · 設計 · 個人開発 · 静的サイト
テンプレートを通らない1枚が、改善から取り残される
静的サイトジェネレータを自作して、記事28ページに canonical と hreflang を入れました。
日本語と英語で14本ずつあり、それまでは検索側から「翻訳」ではなく「別々の記事」に見えていたからです。
生成は一発で通りました。28ページすべてに入りました。
入っていないページが2枚ありました。
抜けていたのは、いちばんリンクされているページだった
トップページと、その英語版です。
site/index.html ← 製品ページ(日本語)
site/index.en.html ← 製品ページ(英語)
site/articles/*.html ← 記事14本
site/en/articles/*.html ← 記事14本
記事は page_shell() という共通の関数を通って生成されます。
LP 2枚は手書きの HTML です。 ジェネレータを書く前に作ったもので、
デザインが1枚ものなので、テンプレートに載せる意味がありませんでした。
だから page_shell() に注釈を足したとき、この2枚には何も起きませんでした。
抜けていたのはこれだけではありません。
| 記事28ページ | LP 2枚 | |
|---|---|---|
rel="canonical" |
有り | 無し |
hreflang |
有り | 無し |
og:title / og:description |
有り | 無し |
og:url / og:type |
有り | 無し |
og:image |
有り | 有り |
twitter:card |
有り | 有り |
og:image と twitter:card だけがあります。手で書いたときに入れたからです。
そのあと記事側で追加したものが、全部抜けています。
結果として、共有カードは画像だけが出てタイトルは推測されるという状態でした。
og:title が無いと、クローラは <title> やページ内の見出しから適当に拾います。
そして、この2枚はリポジトリ3つの README と、記事24本のフッタの行き先です。 サイトの中でいちばんリンクされているページが、いちばん注釈が薄いページでした。
「例外として手で書いたもの」は、改善を受け取らない
一般化するとこうです。
改善はテンプレートに入ります。テンプレートを通らないものには入りません。
手で書いた時点では、たいてい手書きのほうが品質が高い。 時間をかけて、そのページのためだけに作るからです。そこは間違っていません。
問題はそのあとです。
2月 LP を手で書く … 品質は LP のほうが高い
5月 テンプレートに OGP を追加 … 記事だけ良くなる
7月 テンプレートに構造化データ … 記事だけ良くなる
9月 テンプレートに hreflang … 記事だけ良くなる
手書きのページは、作った日の品質のまま止まります。 まわりが上がっていくので、相対的には下がり続けます。
同じ形は、コードのあちこちにあります。
- 共通のエラーハンドラを通さない、1箇所だけの
try/catch - 共通の HTTP クライアントを使わない、1箇所だけの
fetch - 共通のログ関数を使わない、1箇所だけの
console.log - CI を通らない、手で叩くだけのスクリプト
どれも書いた時点では合理的な判断です。 そして、そのあとに入る改善を全部取り逃します。
入力を揃えるより、出力を検査するほうが確実
対策は2つ考えられます。
A. 手書きを無くして、全部テンプレートに寄せる
正しい方向ですが、いつも可能とは限りません。 私の LP は1枚もののデザインで、記事用のテンプレートに載せると壊れます。 載せるためにテンプレートを汎用化すると、記事側の生成が複雑になります。 2枚のために28ページを複雑にするのは、割に合いません。
B. 生成物を、生成の仕方によらず検査する
こちらを取りました。「どう作ったか」ではなく「何ができたか」を見ます。
hreflang には相互性の要件があります。
A が「英語版は B だ」と言うなら、B も「日本語版は A だ」と言わなければ、
注釈ごと無視されます。 片側だけの宣言は効きません。
出力された HTML を全部読んで、これを確かめます。
import re, pathlib
BASE = "https://example.github.io"
site = pathlib.Path("site")
ann = {}
for f in site.rglob("*.html"):
text = f.read_text(encoding="utf-8")
url = BASE + "/" + str(f.relative_to(site))
if f.name == "index.html":
url = url.replace("/index.html", "/")
# lang -> href
alts = {m.group(1): m.group(2) for m in re.finditer(
r'<link rel="alternate" hreflang="([^"]+)" href="([^"]+)"', text)}
if alts:
ann[url] = alts
bad = []
for url, alts in ann.items():
for lang, target in alts.items():
if lang == "x-default":
continue
if not target.startswith(BASE):
continue # 外部は相手が返さないので対象外
if target not in ann:
bad.append(f"{url} -> {target} 相手に hreflang が無い")
elif url not in ann[target].values():
bad.append(f"{url} -> {target} 相手がこちらを指していない")
print(f"hreflang を持つページ: {len(ann)} 不整合: {len(bad)}")
for b in bad:
print(" ", b)
この検査は「テンプレートを通ったかどうか」を知りません。
知る必要がありません。手書きだろうと生成だろうと、
site/ に出た HTML を全部同じ基準で見ます。
入力の経路を管理しようとすると、経路が増えるたびに管理が漏れます。 出力を1箇所で見れば、経路が何本あっても漏れません。
相互性が成立しない相手には、それでも向ける
検査を回すと、こういう結果になりました。
hreflang を持つページ: 32
外部を指す注釈: 20 ← 相互性は成立しない
自サイト内の不整合: 0
記事は同じ内容を外部の媒体にも出していて、そちらを canonical にしています。
すると hreflang も外部の URL を指すことになりますが、
外部の媒体はこちらへ hreflang を返してくれません。 相互性は成立しません。
それでも外を指しています。理由は、選択肢がこの2つしかないからです。
| 相互性 | canonical との整合 | |
|---|---|---|
| 外部の URL を指す | 成立しない(注釈が無視される) | 一致する |
| 自分の非正規版を指す | 成立する | 矛盾する |
hreflang と canonical が矛盾しているページは、
どちらの指示も信用されなくなります。 無視されるほうがまだ軽い。
「効かないと分かっているが、害が少ないほう」を選ぶ場面があります。 このとき大事なのは、効かないと分かったうえで選ぶことです。 知らずに置いてあるのと、結果は同じでも、次に直せるかどうかが違います。
まとめ
- 改善はテンプレートに入る。テンプレートを通らないものには入らない
- 手書きの例外は、**作った日の品質のまま止まる。**まわりが上がるので相対的には下がる
- いちばん手をかけたページが、いちばん取り残されやすい
- 対策は入力を揃えるより、出力を検査するほうが確実。経路が増えても漏れない
hreflangには相互性が要る。片側だけの宣言は効かないhreflangとcanonicalが矛盾するくらいなら、無視されるほうがよい
あわせて読む
Claude Code に設計・実装・レビューを別々の人格として分担させ、GitHub Issue と
ブランチを軸に並列開発を回すための設定一式を MIT で公開しています。
コピーして ./setup.sh を叩けば動きます。技術スタックには依存しません。
https://github.com/quintetkit/quartet
このワークフローだけで実際にツールを 1 つ作りました。Issue の分割から PR、 レビュー、マージまで記録が全部残っています。うまくいかなかった箇所も消していません。
https://github.com/quintetkit/mdlinkcheck
UI 設計人格・レビュー基準・Issue 単位の並列実行スクリプト・実践ガイド 10 章を 足したものは製品ページにあります。
実践ガイド全10章は Zenn Book で読めます(¥1,500・2章まで無料)。
https://zenn.dev/quintetkit/books/claude-code-parallel-workflow
設定一式(5人格・スクリプト・ガイド全文)の配布はこちらです。