Quartet / Quintet

2026-09-05  ·  html · seo · 設計 · 個人開発 · 静的サイト

テンプレートを通らない1枚が、改善から取り残される

静的サイトジェネレータを自作して、記事28ページに canonicalhreflang を入れました。 日本語と英語で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:imagetwitter:card だけがあります。手で書いたときに入れたからです。 そのあと記事側で追加したものが、全部抜けています。

結果として、共有カードは画像だけが出てタイトルは推測されるという状態でした。 og:title が無いと、クローラは <title> やページ内の見出しから適当に拾います。

そして、この2枚はリポジトリ3つの README と、記事24本のフッタの行き先です。 サイトの中でいちばんリンクされているページが、いちばん注釈が薄いページでした。

「例外として手で書いたもの」は、改善を受け取らない

一般化するとこうです。

改善はテンプレートに入ります。テンプレートを通らないものには入りません。

手で書いた時点では、たいてい手書きのほうが品質が高い。 時間をかけて、そのページのためだけに作るからです。そこは間違っていません。

問題はそのあとです。

2月  LP を手で書く         … 品質は LP のほうが高い
5月  テンプレートに OGP を追加  … 記事だけ良くなる
7月  テンプレートに構造化データ  … 記事だけ良くなる
9月  テンプレートに hreflang    … 記事だけ良くなる

手書きのページは、作った日の品質のまま止まります。 まわりが上がっていくので、相対的には下がり続けます。

同じ形は、コードのあちこちにあります。

どれも書いた時点では合理的な判断です。 そして、そのあとに入る改善を全部取り逃します。

入力を揃えるより、出力を検査するほうが確実

対策は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 を指す 成立しない(注釈が無視される) 一致する
自分の非正規版を指す 成立する 矛盾する

hreflangcanonical が矛盾しているページは、 どちらの指示も信用されなくなります。 無視されるほうがまだ軽い。

「効かないと分かっているが、害が少ないほう」を選ぶ場面があります。 このとき大事なのは、効かないと分かったうえで選ぶことです。 知らずに置いてあるのと、結果は同じでも、次に直せるかどうかが違います。

まとめ


あわせて読む


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人格・スクリプト・ガイド全文)の配布はこちらです。

https://quartet-dev.booth.pm/items/8807156

この運用そのものを配っています

4 人格版 Quartet は MIT で無料公開しています。UI 設計人格・レビュー基準・ Issue 単位の並列実行スクリプト・実践ガイド 10 章を足した Quintet は有料版です。

無料版を見る 製品ページ