第14章: SEO対策・レンダリング戦略・canonical
Wicket のデフォルト設定のまま公開サイトを運用すると、Google Search Console で 「ページにリダイレクトがあります」という理由でページがインデックスされないことがあります。 この章では、その原因であるレンダリング戦略と、あわせて対策すべき canonical タグについて解説します。
レンダリング戦略とは
Wicket はページをブラウザに返す際に、内部でいくつかの「レンダリング戦略」を持っています。
RequestCycleSettings の RenderStrategy 列挙型で定義されており、
WicketApplication.init() で変更できます。
| 戦略 | 動作 | デフォルト |
|---|---|---|
REDIRECT_TO_BUFFER |
ページをバッファに描画してから 302 リダイレクトを返し、2回目のリクエストでバッファを送信する | デフォルト |
ONE_PASS_RENDER |
ページを1回のリクエストで直接描画して返す。リダイレクトは発生しない | — |
REDIRECT_TO_BUFFER(デフォルト)
デフォルトの REDIRECT_TO_BUFFER では、ページをリクエストするたびに次のような2ステップの通信が発生します。
通信の流れ:
ブラウザ/クローラー Wicket
| |
| GET /detail/1234 |
|───────────────────────────>|
| | ページをメモリにレンダリング
| | バッファに保存
| 302 Redirect /detail/1234?0| ← リダイレクト発生!
|<───────────────────────────|
| |
| GET /detail/1234?0 |
|───────────────────────────>|
| 200 OK + HTML |
|<───────────────────────────|
この戦略が採用された背景は、フォーム二重送信の防止です。 フォームを POST した後にリダイレクトを挟むことで(Post-Redirect-Get パターン)、 ブラウザの「再読み込み」でも二重送信が起きないようにしています。 インタラクティブなユーザー操作が多い管理画面では有効な設計です。
問題: Google などのクローラーは最初の 302 レスポンスを受け取った時点で
「このURLはリダイレクトされる」と判断します。リダイレクト先(?0 付きURL)を
正規ページとして扱う場合もありますが、元のクリーンなURLは
「ページにリダイレクトがあります」 として未インデックスになります。
ONE_PASS_RENDER
ONE_PASS_RENDER では、ページを1回のリクエストで直接レスポンスとして返します。
ブラウザ/クローラー Wicket
| |
| GET /detail/1234 |
|───────────────────────────>|
| | ページをレンダリング
| 200 OK + HTML | ← 直接返す
|<───────────────────────────|
設定方法は WicketApplication.init() に1行追加するだけです。
import org.apache.wicket.settings.RequestCycleSettings;
@Override
public void init() {
super.init();
// リダイレクトなしで直接レンダリング(SEO 対策)
getRequestCycleSettings().setRenderStrategy(
RequestCycleSettings.RenderStrategy.ONE_PASS_RENDER);
mountPage("/detail/${companyId}", DetailPage.class);
// ... 以下略
}
フォーム二重送信への影響: ONE_PASS_RENDER では PRG パターンが自動適用されません。
データを変更するフォーム(登録・更新・削除)では、onSubmit() の末尾で
setResponsePage() を呼んでリダイレクトすることで、引き続き二重送信を防止できます。
閲覧のみのページ(検索・一覧・詳細表示)では問題ありません。
SEO への影響
REDIRECT_TO_BUFFER が SEO に与える影響をまとめます。
| REDIRECT_TO_BUFFER | ONE_PASS_RENDER | |
|---|---|---|
| クローラーへのレスポンス | 302 → 再リクエスト | 200 直接 |
| Google Search Console の表示 | 「ページにリダイレクトがあります」で未インデックス | 正常にインデックス |
| クロール消費 | 1ページに2リクエスト | 1ページに1リクエスト |
| サーバー負荷 | やや高い(バッファリングあり) | 低い |
| フォーム二重送信防止 | 自動(PRG パターン) | 手動で setResponsePage() が必要 |
コンテンツを公開する一般向けサイトでは ONE_PASS_RENDER が適しています。
外部公開しない管理画面や、フォーム操作が中心のシステムでは REDIRECT_TO_BUFFER のままでも問題ありません。
canonical タグとは
同じコンテンツが複数の URL でアクセスできる場合に、「この URL が正式版です」 と検索エンジンに伝えるための HTML タグです。
<link rel="canonical" href="https://example.com/detail/1234" />
Wicket では、ページ遷移時のナビゲーションパラメータ(呼び出し元情報など)が URL に付加されることがあります。 例えば同じ詳細ページでも、辿った経路によって次のような URL が生成されます。
https://example.com/detail/1234
https://example.com/detail/1234?caller=top&fromCompanyId=0
https://example.com/detail/1234?caller=sitemap
canonical タグがなければ、Google はこれらを別々のページとして評価する可能性があります。 canonical タグを付けることで、どの URL でアクセスされても「正式版はこの URL」と明示できます。
canonical の実装
Wicket での実装は、BasePage に renderHead() をオーバーライドして
canonical タグを動的に出力するのが基本パターンです。
BasePage への追加
Wicket では、サイト全体の共通レイアウト(ヘッダー・フッター・ナビゲーションなど)を
BasePage という親クラスにまとめ、各ページがそれを継承するのが一般的なパターンです。
BasePage に renderHead() を実装することで、
全ページに自動的に canonical タグが出力されます。
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.StringHeaderItem;
import org.apache.wicket.protocol.http.servlet.ServletWebRequest;
import org.apache.wicket.request.cycle.RequestCycle;
public class BasePage extends WebPage {
/**
* canonical URL のパス部分を返す。
* サブクラスでオーバーライドして設定する。
* null を返すと canonical タグは出力されない。
*/
protected String getCanonicalPath() {
return null;
}
@Override
public void renderHead(IHeaderResponse response) {
super.renderHead(response);
String path = getCanonicalPath();
if (path != null) {
// リバースプロキシ(nginx 等)配下でも正しいスキームとホストを取得
ServletWebRequest swr =
(ServletWebRequest) RequestCycle.get().getRequest();
jakarta.servlet.http.HttpServletRequest req =
swr.getContainerRequest();
String proto = req.getHeader("X-Forwarded-Proto");
if (proto == null || proto.isBlank()) proto = req.getScheme();
String host = req.getHeader("X-Forwarded-Host");
if (host == null || host.isBlank()) host = req.getHeader("Host");
if (host == null || host.isBlank()) host = req.getServerName();
response.render(StringHeaderItem.forString(
"<link rel=\"canonical\" href=\""
+ proto + "://" + host + path + "\" />\n"));
}
}
}
X-Forwarded-Proto / X-Forwarded-Host を優先して読んでいるのは、
nginx などが HTTPS を終端している場合に、アプリ側が http://localhost
と誤認するのを防ぐためです。
本番環境では https://example.com、開発環境では http://localhost:8080
と自動で切り替わります。
各ページでのオーバーライド
各ページは getCanonicalPath() をオーバーライドして、そのページのパスを返します。
// 固定パスのページ(TopPage, Updates など)
public class TopPage extends BasePage {
@Override
protected String getCanonicalPath() {
return "/top";
}
}
// パラメータを含むページ(DetailPage など)
public class DetailPage extends BasePage {
private long companyId = -1;
@Override
protected String getCanonicalPath() {
if (companyId == -1) return null;
return "/detail/" + companyId;
}
}
// インデックスされなくていいページ(Login など)
public class Login extends BasePage {
// getCanonicalPath() はオーバーライドしない → null → タグ出力なし
}
このパターンにより、各ページで独立して canonical URL を制御できます。
まとめ
| 対策 | 効果 | 設定箇所 |
|---|---|---|
ONE_PASS_RENDER に変更 |
302 リダイレクトが発生しなくなり、クローラーが直接 200 を受け取れる | WicketApplication.init() |
| canonical タグを追加 | ナビゲーションパラメータ付き URL などの重複を正規化し、評価を集中させる | BasePage.renderHead() + 各ページ |
2つの対策は独立して機能します。ONE_PASS_RENDER はリダイレクト問題を根本から解消し、
canonical はそれでも残りうる URL の揺れを吸収します。
公開コンテンツを持つサイトではどちらも設定しておくことを推奨します。
よくある問題と対策
-
問題Google Search Console で多数のページが「ページにリダイレクトがあります」で未インデックスになっている。原因デフォルトの
REDIRECT_TO_BUFFERが有効で、全ページへのアクセス時に 302 リダイレクトが発生している。対策WicketApplication.init()にONE_PASS_RENDERを設定する。 -
問題canonical タグに
http://localhost:8080が出力されてしまう。原因nginx が HTTPS を終端しており、アプリに届くリクエストがhttpになっている。X-Forwarded-Protoを読んでいない。対策req.getHeader("X-Forwarded-Proto")を優先して読み、なければreq.getScheme()にフォールバックする。 -
問題
ONE_PASS_RENDERに変更したらフォーム送信後に再読み込みで二重登録が発生した。原因PRG パターンが自動適用されなくなり、POST 後にそのまま画面を返している。対策データを変更するonSubmit()の末尾でsetResponsePage()を呼ぶ。閲覧系ページは対応不要。 -
問題canonical タグは設定したが、Search Console でまだ「リダイレクト」扱いになっている。原因canonical タグはリダイレクト問題を根本解消しない。302 が発生している限り、クローラーはリダイレクトと判定する。対策
ONE_PASS_RENDERの設定が漏れていないか確認する。canonical は補助的な対策として組み合わせる。