Remixの多言語サイトで英語ページのクリックが0だった話とMCPでの解決記録

ある朝、Google Search Console Team からメールが2通届きました。件名は「ページがインデックスに登録されない」。開いてみると、「重複しています。Googleにより、ユーザーがマークしたページとは異 […]

広告ここから
広告ここまで

目次

    ある朝、Google Search Console Team からメールが2通届きました。件名は「ページがインデックスに登録されない」。開いてみると、「重複しています。Googleにより、ユーザーがマークしたページとは異なるページが正規ページとして選択されました」と「代替ページ(適切なcanonicalタグあり)」という通知でした。

    wp-kyoto.net は Remix + Cloudflare Pages で動く日英2言語対応のWordPressコンテンツサイトです。Search Console を確認すると、英語ページがインプレッション数百件あるのにクリックが0に近い。ルーティングは動いていたので気づいていませんでした。原因を調べて修正した記録を残しておきます。

    コードを読んで仮説を立てたまず実装の問題かどうかを確認したかったため、コードから調査しました。

    /ja/ リダイレクトが 302(一時的)

    // app/utils/lang.ts
    export const handlePostLanguage = (path: string, lang?: string) => {
      if (lang === 'ja') {
        throw redirect(`/${path}`, 302);  // ← 302になっていた
      }
    };
    

    /ja/記事スラッグ にアクセスすると /記事スラッグ に302でリダイレクトする実装でした。302は「一時的な移動」を意味するため、Googleはリダイレクト元URLを有効なURLとして保持し続けます。

    hreflang タグが存在しない

    // ($lang).$post_slug.tsx の meta 関数
    return [
      { title: title },
      { tagName: 'link', rel: 'canonical', href: url },
      // hreflang が一切ない
    ];
    

    日本語版 /slug と英語版 /en/slug の関係をGoogleに伝える手段がありませんでした。rel-alternate-hreflang アノテーションは、同一コンテンツの多言語バリアントであることをGoogleに伝えるタグです。

    ページネーションURLに meta がない

    // app/routes/($lang).pages.$page.tsx
    // meta エクスポートが存在しない → canonical も hreflang もなし
    

    コードを読んだ時点で3つの問題が見つかりましたが、これが本当に影響しているかをデータで確認することにしました。

    MCPで実データを裏どりした

    Search Console MCP で実際のデータを引いて確認しました。

    英語ページが大量インプレッション・クリック0

    /en/building-subscription-system-with-trial-using-clerk-and-stripe
      → 762インプレッション、0クリック
    
    /en/remix-vercel-ai-sdk-zod-dependency
      → 498インプレッション、0クリック
    

    検索結果には表示されているのにクリックされない状態でした。Googleが英語ページを「代替ページ」扱いしてランキングを下げているためです。

    同一スラッグの日英ページが重複判定されている

    /fetch-nextjs-api-on-app-router     → 日本語版 5クリック
    /en/fetch-nextjs-api-on-app-router  → 英語版 0クリック、87インプレ
    

    hreflangがないため、Googleが両者を重複コンテンツと判定していました。エンゲージメントが高い日本語版を正規として選択し、英語版を除外した結果です。

    コード解析では気づいていなかった:ページネーションURLもインデックスされている

    /en/pages/2  → 12インプレッション
    /en/pages/7  → 3インプレッション
    

    canonicalもhreflangもないページネーションURLがGoogleにクロールされていました。データで見て初めて把握した問題です。

    指摘された内容を修正する

    3つの修正を1つのPRにまとめました。302→301は軽微な変更だったので、hreflang追加と同一PRに混ぜても問題ないと判断しました。

    リダイレクトを 301 にする

    // Before
    if (lang === 'ja') {
      throw redirect(`/${path}`, 302);
    }
    
    // After
    if (lang === 'ja') {
      throw redirect(`/${path}`, 301);
    }
    

    記事ページに hreflang を追加する

    { tagName: 'link', rel: 'canonical', href: url },
    { tagName: 'link', rel: 'alternate', hrefLang: 'ja', href: `${baseUrl}/${slug}` },
    { tagName: 'link', rel: 'alternate', hrefLang: 'en', href: `${baseUrl}/en/${slug}` },
    { tagName: 'link', rel: 'alternate', hrefLang: 'x-default', href: `${baseUrl}/${slug}` },
    

    ページネーションにも canonical / hreflang を追加する

    AIコーディングで運用開発している以上、同じ問題が再発しかねないため、テストでデグレしないようにしました。

    // app/utils/pagination-meta.ts
    export function buildPaginationMeta(
      lang: string,
      page: number,
      baseUrl: string
    ): MetaDescriptor[] {
      const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
      const jaUrl = `${normalizedBaseUrl}/pages/${page}`;
      const enUrl = `${normalizedBaseUrl}/en/pages/${page}`;
      const canonicalUrl = lang === 'en' ? enUrl : jaUrl;
    
      return [
        { title: `Page ${page} | WP Kyoto` },
        { tagName: 'link', rel: 'canonical', href: canonicalUrl },
        { tagName: 'link', rel: 'alternate', hrefLang: 'ja', href: jaUrl },
        { tagName: 'link', rel: 'alternate', hrefLang: 'en', href: enUrl },
        { tagName: 'link', rel: 'alternate', hrefLang: 'x-default', href: jaUrl },
      ];
    }
    

    テストは Red → Green の順で実装しました。

    ✓ 日本語ページのcanonical URLが /pages/N 形式で生成される
    ✓ 英語ページのcanonical URLが /en/pages/N 形式で生成される
    ✓ hreflang="ja" が日本語URL (/pages/N) を指す
    ✓ hreflang="en" が英語URL (/en/pages/N) を指す
    ✓ hreflang="x-default" が日本語URL (/pages/N) を指す
    ✓ baseUrl末尾にスラッシュがあっても二重スラッシュにならない
    ✓ hreflang URLも末尾スラッシュの影響を受けない
    

    まとめ

    多言語ルーティングが動いていても、GoogleにはそれぞれのURLが独立したページにしか見えていませんでした。「機能する」と「Googleに正しく伝わる」は別の問題です。

    hreflangはGoogleへの「ヒント」なので、修正しても即座に検索順位が変わるとは限りません。変化はSearch Consoleで数週間後に確認します。

    Remix / Next.js で多言語サイトを運営している場合のチェックリストです。

    □ hreflang タグを全ページに設定しているか
        ja, en, x-default の3つセット
    
    □ 言語リダイレクトは 302 ではなく 301 か
    
    □ optional な言語パラメータ([lang]? や ($lang))を使っている場合、
      同一スラッグが複数URLで露出していないか
    
    □ ページネーション・カテゴリ・タグページにも canonical を設定しているか
    
    □ Search Console で英語ページのクリック数を確認しているか
      → インプレッション >> クリック になっていたら要調査
    

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark