🔥

ライフサイクルとSEOで失敗したくない!Nuxt3×Vue3のSSRとCSRの基本解説

に公開

最近、Nuxt3 × Vue3でトレーミーというジムの検索サービスを作っていて、
SSRとCSR の動きを把握していなかったせいでSEOで痛い目を見ました。

https://toreme.com/

「非同期で取ってるデータがGooglebotになぜか評価されない」
「CSRでしか動かない処理が原因で、初回レンダリングが空っぽな部分がある」
— 気付いたときにはインデックスされておらず、アクセスが全然伸びない…。

自分と同じ落とし穴にはまらないために、
Nuxt3 の SSR/CSR の仕組みとライフサイクルを整理し、
何を押さえれば SEO で失敗しないかをまとめました。


Googlebot はどうレンダリングしているか

SSR/CSR を気にする大きな理由はSEOです。
特に、非同期で読み込んだデータが Googlebot に認識されない問題は要注意です。

Google公式の解説 でも触れられていますが、
JavaScript によるレンダリングの多くはGooglebotにも認識される とはいえ、
「重要でない」と判断されるとクロールされない可能性もあります。


そもそもGoogleはユーザーで同じように認識をするようになってきている

Googleは現在、最新のウェブ ブラウザに近いコンテンツの表示、外部リソースの追加、JavaScript の実行、CSSの適用を実現しています。
参考: Google Developers

Googleも人間と同じように最適化されています。しかし、Google企業側は年間5兆回を超えるトラフィックを処理していることもあり、コンピューター負荷が高いのは間違いありません。
このコストを多少下げることで年間何億のコスト削減ができることは容易に考えられると思います。
そのため、私たち開発者は、Googleが認識しやすいような形式にすることで、コンピューター負荷を下げGoogleのためになるサイトが作らなければ優遇されないことは想像の範囲内かなと考えています。
特にGoogleも費用対効果がなさそうな重い処理は無視をするだろうと私は考えています。
(このあたり公式ドキュメントがなかったので、ありましたら教えてください。)

そのため、いくら人間と同じように認識をしてくれるとは言えども、Google側が認識しやすいようにWebサイトの構造を作らなければ、SEO的に優遇されにくくなってしまいます。

ここまでを前提として抑えたうえで、SSR/CSRの基本から以下に書いていきます。


Nuxt3 における SSR と CSR の基本

Nuxt3 では SSR (Server Side Rendering) と CSR (Client Side Rendering) が共存しています。
両者の違いと、なぜ使い分けが必要なのかを一度整理しておきます。

  • SSR (Server Side Rendering)
    サーバー側でページを生成し、HTML を返す方式。
    Googlebot にとってもクロールしやすく、コンテンツのローディング量が少ないため初回表示が早くなる。(例外有)

  • CSR (Client Side Rendering)
    JavaScript が実行されてからコンテンツを生成する方式。
    ※SSR/CSRの組み合わせの際はSSR側からHTMLを受け取り、さらにJavaScriptがブラウザで実行される。
    ページ遷移が速く、インタラクティブな UI を作りやすい。


SSR/CSR が確認しにくい理由

実際に開発していると、「あれ、これはSSRで動いてる?CSRで後から呼ばれてる?」
と迷う場面がよくあります。
その理由の一つとして、NuxtがSSRとCSRの区別がつきにくいライフサイクルをしていることがあげられます。
例えば、同ファイル(pages/index.vueなど)をSSRでもCSRでも参照をするなど。

特にNuxt3では、nuxt.config.tsでSSRを設定している場合に、初回アクセス時は SSR が動きますが、内部ナビゲーションではCSR になります。
この切り替わりを理解しないと、想定と違う挙動になります。
後続にNuxt3のライフサイクルについて整理したものと、SSRの確認方法を上げます。


Nuxt3 のライフサイクルを整理する

SSR/CSR の動きを把握するには、Nuxt3/Vue3のライフサイクルを正しく知るのが一番です。

以下が代表的な流れです。

SSR 時に実行される処理

  1. plugins
  2. middleware
    2.1 Global middleware
    2.2 Layout middleware
    2.3 Route middleware
  3. setup()
    3.1. useAsyncData()
    3.2 useFetch()
    3.3 onServerPrefetch()

ここまで終わったらブラウザ側のCSRが実行され始めます。

CSR 時に実行される処理

  1. plugins
  2. middleware
    2.1 Global middleware
    2.2 Layout middleware
    2.3 Route middleware
  3. setup()
    3.1. useAsyncData()
    3.2 useFetch()
  4. onBeforeMount()
  5. onMounted()
  6. onBeforeUpdate() [更新時のみ]
  7. onUpdated() [更新時のみ]
  8. onBeforeUnmount()
  9. onUnmounted()

内部ナビゲーション時

画面遷移時には SSR は走らず、CSR のみで完結します。

  1. middleware
    1.1 Global middleware
    1.2 Layout middleware
    1.3 Route middleware
  2. setup()
    2.1. useAsyncData()
    2.2 useFetch()
  3. onBeforeMount()
  4. onMounted()
  5. onBeforeUpdate() [更新時のみ]
  6. onUpdated() [更新時のみ]
  7. onBeforeUnmount()
  8. onUnmounted()

ここからわかることは、plugins は内部遷移時には動かないため、共通処理が必要なら middleware を活用していくことで、課題を解決することができるということです。
またmiddlewareはSSR/CSR両方で通るため、SSRの際の処理とCSRの処理を同梱している1つのファイルで開発をしているとレンダリングした際に、CSR処理なのにサーバー側の情報を参照してエラーになる場合があります。

本題から外れるため、詳細を知りたい方はこちらを参照して下さい。
1.Nuxt.jsのmiddlewareディレクトリについて詳しく解説
2.middleware · Nuxt Directory Structure v3

また、useAsyncDataはSSRでもCSRでも動作しています。
そうなると、「2回実行してるんじゃないの!?」となると思いますが、キーを指定して結果をキャッシュして再利用しているので、多重リクエストは問題ありません。

//'posts'と書いてあるのがキー
await useAsyncData('posts', () => $fetch('/api/posts'))

SSR/CSR の動きを確認する方法

「ちゃんと SSR でレンダリングされてるのか?」は
Chrome DevTools の Network タブ で確認するのが鉄板です。

  1. Network タブを開く
  2. ページをリロードして HTML を確認(多くは独自ドメインやlocalhostという表記になっている。)
  3. PreviewResponse を比較
  4. 実際にブラウザに表示されている DOM と突き合わせる
    実際に問題のない確認画面

よくある落とし穴と対策例

最後に、自分がハマったポイントを挙げておきます。

  • SEO設定がCSRで動いていて、うまく反映されていなかった
    Nuxt3/Vue3でSEOの設定をする際は以下のように行います。
[pages/index.vue]
/**
 * 引数:
 * @param title          ページタイトル(オプション)
 * @param description    ページ説明(オプション)
 * @param image          OGP画像URL(オプション)
 * @param canonicalPath  カノニカルURLパス(オプション)
 */

useHead({
    htmlAttrs: {
        lang: 'ja',
    },
    title: title ? `${title} | トレーミー` : 'トレーミー',
    meta: [
        { name: 'description', content: description },
        { property: 'og:title', content: title ? `${title} | トレーミー` : 'トレーミー' },
        { property: 'og:description', content: description },
        { property: 'og:image', content: image ?? `${config.public.BASE_URL}/images/root/keyvisual.svg` },
        { property: 'og:url', content: fullUrl },
        { property: 'og:type', content: 'website' },
        { property: 'og:site_name', content: 'トレーミー' },
        // X(Twitter)
        { name: 'twitter:card', content: 'summary_large_image' },
        { name: 'twitter:domain', content: 'toreme.com' },
        { name: 'twitter:title', content: title ? `${title} | トレーミー` : 'トレーミー' },
        { name: 'twitter:description', content: description },
        { name: 'twitter:image', content: image ?? `${config.public.BASE_URL}/images/root/keyvisual.svg` },
    ],
    link: [
        ...(canonicalPath ? [{ rel: 'canonical', href: `${config.public.BASE_URL}${canonicalPath}` }] : []),
    ],
})

//✖ダメな例:onMountedなどのCSR処理の中に入れるとGoogle側の登録時に意図しない登録がされます。
onMounted(() => {
    useHead({
        htmlAttrs: {
            lang: 'ja',
        }
    })
})
  • assets と public の参照先が混乱する
    SSR時にはassets配下が参照され、CSRではpublic 配下を参照するため、
    パスの管理を誤ると404エラーになります。

    現在SSR/CSRどちらの処理でファイルを参照しようとしているのかを意識しながらコードを書きましょう。

  • SSRで認証は通っているのに、CSR側でpiniaを参照してエラー
    SSRとCSRの状態管理の区別がついていないと認証フローで落ちやすいです。
    例えば、middlewareでpiniaにストアした認証情報を取得してから特定のページに行きたいと考えて認証情報をpiniaから取得しようとするとエラーが起きます。
    piniaはブラウザのLocal storageにデータが保存されるため、サーバーサイドからは参照できません。
    状態を正しくハイドレーションするか、永続化ストアを検討しましょう。

auth.client.ts
auth.server.ts
などのファイル分けをしてスコープを特定する

もしくは、ファイル内で分けて判断をしましょう。

[auth.middleware.ts]
export default defineNuxtRouteMiddleware(() => {
  if (import.meta.server) {
    console.log("SSR(サーバーサイド)のミドルウェア処理")
    //サーバーサイドから認証情報を取得
  }
  if (import.meta.client) {
    console.log("CSR(クライアントサイド)のミドルウェア処理")
    //クライアントサイドから認証情報を取得
  }
}

まとめ

Nuxt3 の SSR/CSR は慣れないと地味に落とし穴が多いです。

とくに SEO を意識する場合は、

  • 初回描画で必要なコンテンツは SSR で返す
  • 内部ナビゲーションでも状態がズレない設計にする
  • Googlebot のレンダリング挙動を知る

この3つは最低限押さえておくのがおすすめです。

同じところでハマる人が少しでも減れば嬉しいです!


参考


🙌 ここまで読んでいただきありがとうございます!
気付きやコメントがあればぜひ教えてください。

Discussion