diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 359a35e6..f6d273d9 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -126,6 +126,7 @@ function sidebarGuide() { link: '/guide/extending-default-theme' }, { text: 'Build-Time Data Loading', link: '/guide/data-loading' }, + { text: 'SSR Compatibility', link: '/guide/ssr-compat' }, { text: 'Connecting to a CMS', link: '/guide/cms' } ] }, diff --git a/docs/guide/custom-theme.md b/docs/guide/custom-theme.md index 4937d220..83625a23 100644 --- a/docs/guide/custom-theme.md +++ b/docs/guide/custom-theme.md @@ -66,7 +66,7 @@ export default { The default export is the only contract for a custom theme, and only the `Layout` property is required. So technically, a VitePress theme can be as simple as a single Vue component. -Inside your layout component, it works just like a normal Vite + Vue 3 application. Do note the theme also needs to be [SSR-compatible](./using-vue#browser-api-access-restrictions). +Inside your layout component, it works just like a normal Vite + Vue 3 application. Do note the theme also needs to be [SSR-compatible](./ssr-compat). ## Building a Layout diff --git a/docs/guide/ssr-compat.md b/docs/guide/ssr-compat.md new file mode 100644 index 00000000..9a97c940 --- /dev/null +++ b/docs/guide/ssr-compat.md @@ -0,0 +1,84 @@ +--- +outline: deep +--- + +# SSR Compatibility + +VitePress pre-renders the app in Node.js during the production build, using Vue's Server-Side Rendering (SSR) capabilities. This means all custom code in theme components are subject to SSR Compatibility. + +The [SSR section in official Vue docs](https://vuejs.org/guide/scaling-up/ssr.html) provides more context on what is SSR, the relationship between SSR / SSG, and common notes on writing SSR-friendly code. The rule of thumb is to only access browser / DOM APIs in `beforeMount` or `mounted` hooks of Vue components. + +## `<ClientOnly>` + +If you are using or demoing components that are not SSR-friendly (for example, contain custom directives), you can wrap them inside the built-in `<ClientOnly>` component: + +```md +<ClientOnly> + <NonSSRFriendlyComponent /> +</ClientOnly> +``` + +## Libraries that Access Browser API on Import + +Some components or libraries access browser APIs **on import**. To use code that assumes a browser environment on import, you need to dynamically import them. + +### Importing in Mounted Hook + +```vue +<script setup> +import { onMounted } from 'vue' + +onMounted(() => { + import('./lib-that-access-window-on-import').then((module) => { + // use code + }) +}) +</script> +``` + +### Conditional Import + +You can also conditionally import a dependency using the `import.meta.env.SSR` flag (part of [Vite env variables](https://vitejs.dev/guide/env-and-mode.html#env-variables)): + +```js +if (!import.meta.env.SSR) { + import('./lib-that-access-window-on-import').then((module) => { + // use code + }) +} +``` + +Since [`Theme.enhanceApp`](/guide/custom-theme#theme-interface) can be async, you can conditionally import and register Vue plugins that access browser APIs on import: + +```js +// .vitepress/theme/index.js +export default { + // ... + async enhanceApp({ app }) { + if (!import.meta.env.SSR) { + const plugin = await import('plugin-that-access-window-on-import') + app.use(plugin) + } + } +} +``` + +### `defineClientComponent` + +VitePress provides a convenience helper for importing Vue components that access browser APIs on import. + +```vue +<script setup> +import { defineClientComponent } from 'vitepress' + +const ClientComp = defineClientComponent(() => { + return import('component-that-access-window-on-import') +}) +</script> + +<template> + <ClientComp /> +</template> +``` + +The target component will only be imported in the mounted hook of the wrapper component. diff --git a/docs/guide/using-vue.md b/docs/guide/using-vue.md index 29975389..947ebeff 100644 --- a/docs/guide/using-vue.md +++ b/docs/guide/using-vue.md @@ -4,6 +4,10 @@ In VitePress, each Markdown file is compiled into HTML and then processed as a [ It's worth noting that VitePress leverages Vue's compiler to automatically detect and optimize the purely static parts of the Markdown content. Static contents are optimized into single placeholder nodes and eliminated from the page's JavaScript payload for initial visits. They are also skipped during client-side hydration. In short, you only pay for the dynamic parts on any given page. +:::tip SSR Compatibility +All Vue usage needs to be SSR-compatible. See [SSR Compatibility](./ssr-compat) for details and common workarounds. +::: + ## Templating ### Interpolation @@ -216,63 +220,6 @@ Then you can use the following in Markdown and theme components: </style> ``` -## Browser API Access Restrictions - -Because VitePress applications are server-rendered in Node.js when generating static builds, any Vue usage must conform to the [universal code requirements](https://vuejs.org/guide/scaling-up/ssr.html). In short, make sure to only access Browser / DOM APIs in `beforeMount` or `mounted` hooks. - -If you are using or demoing components that are not SSR-friendly (for example, contain custom directives), you can wrap them inside the built-in `<ClientOnly>` component: - -```md -<ClientOnly> - <NonSSRFriendlyComponent /> -</ClientOnly> -``` - -Note this does not fix components or libraries that access Browser APIs **on import**. To use code that assumes a browser environment on import, you need to dynamically import them in proper lifecycle hooks: - -```vue -<script> -export default { - mounted() { - import('./lib-that-access-window-on-import').then((module) => { - // use code - }) - } -} -</script> -``` - -If your module `export default` a Vue component, you can register it dynamically: - -```vue -<template> - <component - v-if="dynamicComponent" - :is="dynamicComponent"> - </component> -</template> - -<script> -export default { - data() { - return { - dynamicComponent: null - } - }, - - mounted() { - import('./lib-that-access-window-on-import').then((module) => { - this.dynamicComponent = module.default - }) - } -} -</script> -``` - -**Also see:** - -- [Vue.js > Dynamic Components](https://vuejs.org/guide/essentials/component-basics.html#dynamic-components) - ## Using Teleports Vitepress currently has SSG support for teleports to body only. For other targets, you can wrap them inside the built-in `<ClientOnly>` component or inject the teleport markup into the correct location in your final page HTML through [`postRender` hook](../reference/site-config#postrender). diff --git a/docs/reference/runtime-api.md b/docs/reference/runtime-api.md index 3b7c043f..68bf29cc 100644 --- a/docs/reference/runtime-api.md +++ b/docs/reference/runtime-api.md @@ -121,6 +121,8 @@ If you are using or demoing components that are not SSR-friendly (for example, c </ClientOnly> ``` +- Related: [SSR Compatibility](/guide/ssr-compat) + ## `$frontmatter` <Badge type="info" text="template global" /> Directly access current page's [frontmatter](../guide/frontmatter) data in Vue expressions.