SSR Compatibility
VitePress pre-renders the application in Node.js during build, generating static HTML. This means your code must be compatible with server-side rendering (SSR).
Understanding SSR in VitePress
During build, VitePress:
Runs your code in Node.js (SSR)
Calls renderToString() from Vue’s server renderer
Generates static HTML files
Hydrates on the client side
Reference: /home/daytona/workspace/source/src/client/app/ssr.ts:6
export async function render ( path : string ) {
const { app , router } = await createApp ()
await router . go ( path )
const ctx : SSGContext = { content: '' , vpSocialIcons: new Set < string >() }
ctx . content = await renderToString ( app , ctx )
return ctx
}
Browser-Only APIs
Check the environment before accessing browser APIs:
// Check if running in browser
if ( import . meta . env . DEV ) {
// Development-only code
}
if ( import . meta . env . PROD ) {
// Production-only code
}
if ( import . meta . env . SSR ) {
// Server-side rendering
} else {
// Client-side only
}
The inBrowser Helper
VitePress provides an inBrowser constant:
import { inBrowser } from 'vitepress'
if ( inBrowser ) {
// Safe to use window, document, localStorage
window . addEventListener ( 'scroll' , handleScroll )
}
Reference: /home/daytona/workspace/source/src/shared/shared.ts:35
export const inBrowser = typeof document !== 'undefined'
Client-Only Components
Using ClientOnly Component
Wrap components that rely on browser APIs:
< script setup >
import { ClientOnly } from 'vitepress'
import BrowserOnlyComponent from './BrowserOnlyComponent.vue'
</ script >
< template >
< ClientOnly >
< BrowserOnlyComponent />
</ ClientOnly >
</ template >
How ClientOnly Works
Reference: /home/daytona/workspace/source/src/client/app/components/ClientOnly.ts:1
import { defineComponent , onMounted , ref } from 'vue'
export const ClientOnly = defineComponent ({
setup ( _ , { slots }) {
const show = ref ( false )
onMounted (() => {
show . value = true
})
return () => ( show . value && slots . default ? slots . default () : null )
}
})
ClientOnly renders nothing during SSR and only renders content after the component is mounted on the client.
Async Component Loading
Load components dynamically on the client:
import { defineClientComponent } from 'vitepress'
export default {
components: {
Chart: defineClientComponent (() => import ( './Chart.vue' ))
}
}
defineClientComponent API
Reference: /home/daytona/workspace/source/src/client/app/utils.ts:84
export function defineClientComponent (
loader : AsyncComponentLoader ,
args ?: any [],
cb ?: () => Awaitable < void >
) {
return {
setup () {
const comp = shallowRef ()
onMounted ( async () => {
let res = await loader ()
// interop module default
if ( res && ( res . __esModule || res [ Symbol . toStringTag ] === 'Module' )) {
res = res . default
}
comp . value = res
await cb ?.()
})
return () => ( comp . value ? h ( comp . value , ... ( args ?? [])) : null )
}
}
}
Lifecycle Hooks
Avoid beforeMount and mounted during SSR
These hooks only run on the client: < script setup >
import { onMounted } from 'vue'
onMounted (() => {
// Safe - only runs in browser
document . title = 'My Page'
})
</ script >
Be careful with setup()
setup() runs on both server and client:< script setup >
import { ref } from 'vue'
import { inBrowser } from 'vitepress'
// Runs on both server and client
const count = ref ( 0 )
// Conditional client-only code
if ( inBrowser ) {
count . value = parseInt ( localStorage . getItem ( 'count' ) || '0' )
}
</ script >
Common SSR Issues
Accessing window/document
// This will crash during SSR
const width = window . innerWidth
localStorage/sessionStorage
const token = localStorage . getItem ( 'token' )
CSS-in-JS Libraries
Many CSS-in-JS libraries need special SSR handling:
< script setup >
import { inBrowser } from 'vitepress'
import { onMounted } from 'vue'
let styled
onMounted ( async () => {
// Load CSS-in-JS library only on client
styled = await import ( 'styled-components' )
})
</ script >
Router and Navigation
Safe Route Access
The router is available on both server and client:
< script setup >
import { useRoute , useRouter } from 'vitepress'
const route = useRoute ()
const router = useRouter ()
// Safe on both server and client
console . log ( route . path )
console . log ( route . data . frontmatter )
// Client-only navigation
function navigate () {
router . go ( '/about' )
}
</ script >
Reference: /home/daytona/workspace/source/src/client/app/router.ts:248
export function useRouter () : Router {
const router = inject ( RouterSymbol )
if ( ! router ) throw new Error ( 'useRouter() is called without provider.' )
return router
}
export function useRoute () : Route {
return useRouter (). route
}
Location Hash
Be careful with location.hash during SSR:
import { inBrowser } from 'vitepress'
const hash = inBrowser ? location . hash : ''
Or use VitePress’s reactive hash:
< script setup >
import { useData } from 'vitepress'
const { hash } = useData ()
// hash.value is reactive and SSR-safe
</ script >
Third-Party Libraries
Checking Library Compatibility
Libraries that immediately access browser APIs during import will break SSR.
Option 1: Dynamic Import
< script setup >
import { onMounted , shallowRef } from 'vue'
const lib = shallowRef ()
onMounted ( async () => {
lib . value = await import ( 'browser-only-lib' )
})
</ script >
Option 2: Conditional Import
import { inBrowser } from 'vitepress'
let myLib
if ( inBrowser ) {
myLib = await import ( 'browser-only-lib' )
}
Option 3: Vite’s SSR Externals
export default {
vite: {
ssr: {
noExternal: [ 'library-name' ]
}
}
}
Environment Variables
VitePress exposes environment info via import.meta.env:
// Available in both SSR and client
import . meta . env . MODE // 'development' or 'production'
import . meta . env . DEV // boolean
import . meta . env . PROD // boolean
import . meta . env . SSR // boolean (true during SSR)
import . meta . env . BASE_URL // string
Reference: /home/daytona/workspace/source/src/client/app/router.ts:112
route . data = import . meta . env . PROD
? markRaw ( __pageData )
: ( readonly ( __pageData ) as PageData )
Testing SSR Compatibility
npm run build
npm run preview
Check browser console for hydration mismatches.
ReferenceError: window is not defined - Using browser API during SSR
ReferenceError: document is not defined - Accessing DOM during SSR
Hydration mismatch - Server and client render different content
Enable Vue’s hydration mismatch details: export default {
vite: {
define: {
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true
}
}
}