Skip to content

SSR 互換性

VitePress は本番ビルド時に、Node.js 上で Vue のサーバーサイドレンダリング(SSR)機能を使ってアプリを事前レンダリングします。つまり、テーマコンポーネント内のすべてのカスタムコードは SSR 互換性の対象になります。

公式 Vue ドキュメントの SSR セクションでは、SSR とは何か、SSR と SSG の関係、そして SSR に優しいコードを書く際の一般的な注意点が解説されています。経験則としては、ブラウザ / DOM API へのアクセスは Vue コンポーネントの beforeMount または mounted フック内に限定 するのが安全です。

<ClientOnly>

SSR に適さないコンポーネント(例:カスタムディレクティブを含むなど)を使用・デモする場合は、組み込みの <ClientOnly> コンポーネントでラップできます。

md
<ClientOnly>
  <NonSSRFriendlyComponent />
</ClientOnly>

インポート時に Browser API にアクセスするライブラリ

一部のコンポーネントやライブラリは インポート時に ブラウザ API にアクセスします。インポート時にブラウザ環境を前提とするコードを使うには、動的インポートが必要です。

mounted フック内でのインポート

vue
<script setup>
import { onMounted } from 'vue'

onMounted(() => {
  import('./lib-that-access-window-on-import').then((module) => {
    // ここでコードを利用
  })
})
</script>

条件付きインポート

import.meta.env.SSR フラグ(Vite の環境変数の一部)を使って、依存関係を条件付きでインポートすることもできます。

js
if (!import.meta.env.SSR) {
  import('./lib-that-access-window-on-import').then((module) => {
    // ここでコードを利用
  })
}

Theme.enhanceApp は非同期にできるため、インポート時に Browser API に触れる Vue プラグイン を条件付きでインポート・登録できます。

.vitepress/theme/index.js
js
/** @type {import('vitepress').Theme} */
export default {
  // ...
  async enhanceApp({ app }) {
    if (!import.meta.env.SSR) {
      const plugin = await import('plugin-that-access-window-on-import')
      app.use(plugin.default)
    }
  }
}

TypeScript を使う場合:

.vitepress/theme/index.ts
ts
import type { Theme } from 'vitepress'

export default {
  // ...
  async enhanceApp({ app }) {
    if (!import.meta.env.SSR) {
      const plugin = await import('plugin-that-access-window-on-import')
      app.use(plugin.default)
    }
  }
} satisfies Theme

defineClientComponent

VitePress は、インポート時に Browser API にアクセスする Vue コンポーネント を読み込むためのユーティリティを提供します。

vue
<script setup>
import { defineClientComponent } from 'vitepress'

const ClientComp = defineClientComponent(() => {
  return import('component-that-access-window-on-import')
})
</script>

<template>
  <ClientComp />
</template>

ターゲットコンポーネントに props / children / slots を渡すこともできます。

vue
<script setup>
import { ref } from 'vue'
import { defineClientComponent } from 'vitepress'

const clientCompRef = ref(null)
const ClientComp = defineClientComponent(
  () => import('component-that-access-window-on-import'),

  // 引数は h() に渡されます - https://vuejs.org/api/render-function.html#h
  [
    {
      ref: clientCompRef
    },
    {
      default: () => 'default slot',
      foo: () => h('div', 'foo'),
      bar: () => [h('span', 'one'), h('span', 'two')]
    }
  ],

  // コンポーネント読み込み後のコールバック(非同期可)
  () => {
    console.log(clientCompRef.value)
  }
)
</script>

<template>
  <ClientComp />
</template>

ターゲットコンポーネントは、ラッパーコンポーネントの mounted フックで初めてインポートされます。

MIT ライセンスの下で公開されています。