第14章: SEO対策・レンダリング戦略・canonical

Wicket のデフォルト設定のまま公開サイトを運用すると、Google Search Console で 「ページにリダイレクトがあります」という理由でページがインデックスされないことがあります。 この章では、その原因であるレンダリング戦略と、あわせて対策すべき canonical タグについて解説します。

レンダリング戦略とは

Wicket はページをブラウザに返す際に、内部でいくつかの「レンダリング戦略」を持っています。 RequestCycleSettingsRenderStrategy 列挙型で定義されており、 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_BUFFERONE_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 での実装は、BasePagerenderHead() をオーバーライドして canonical タグを動的に出力するのが基本パターンです。

BasePage への追加

Wicket では、サイト全体の共通レイアウト(ヘッダー・フッター・ナビゲーションなど)を BasePage という親クラスにまとめ、各ページがそれを継承するのが一般的なパターンです。 BasePagerenderHead() を実装することで、 全ページに自動的に 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 は補助的な対策として組み合わせる。