SSR Compatibility
Learn about Server-Side Rendering (SSR) compatibility in VitePress and how to handle SSR-related issues.
Understanding SSR in VitePress
What is SSR?
Server-Side Rendering (SSR) means that your Vue components are rendered on the server during the build process, generating static HTML files. This provides:
- Better SEO: Search engines can crawl the pre-rendered content
- Faster Initial Load: Users see content immediately
- Better Performance: Reduced time to first contentful paint
VitePress SSR Process
VitePress uses SSR during the build process:
- Build Time: Components are rendered on the server
- Static Generation: HTML files are generated with pre-rendered content
- Hydration: Client-side JavaScript takes over for interactivity
Common SSR Issues
Browser-Only APIs
Code that relies on browser APIs will fail during SSR:
// ❌ This will cause SSR errors
const width = window.innerWidth
const element = document.getElementById('app')
localStorage.setItem('key', 'value')
// ✅ Check for browser environment
if (typeof window !== 'undefined') {
const width = window.innerWidth
const element = document.getElementById('app')
localStorage.setItem('key', 'value')
}
Component Lifecycle Issues
Some lifecycle hooks don't run during SSR:
<script setup>
import { onMounted, ref } from 'vue'
const data = ref(null)
// ❌ This won't work during SSR
const browserData = window.location.href
// ✅ Use onMounted for browser-only code
onMounted(() => {
data.value = window.location.href
})
</script>
Third-Party Libraries
Libraries that assume browser environment:
// ❌ Library that uses window/document immediately
import BrowserOnlyLibrary from 'browser-only-lib'
// ✅ Dynamic import in onMounted
import { onMounted } from 'vue'
let library = null
onMounted(async () => {
const { default: BrowserOnlyLibrary } = await import('browser-only-lib')
library = new BrowserOnlyLibrary()
})
SSR-Safe Patterns
Conditional Rendering
Use conditional rendering for browser-only content:
<template>
<div>
<h1>My Page</h1>
<!-- This will be rendered during SSR -->
<p>This content is SSR-safe</p>
<!-- This will only render on the client -->
<ClientOnly>
<BrowserOnlyComponent />
</ClientOnly>
</div>
</template>
<script setup>
import { ClientOnly } from 'vitepress'
import BrowserOnlyComponent from './BrowserOnlyComponent.vue'
</script>
Using ClientOnly Component
The ClientOnly
component prevents SSR rendering:
<template>
<div>
<ClientOnly>
<div>{{ windowWidth }}px</div>
<template #fallback>
<div>Loading...</div>
</template>
</ClientOnly>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ClientOnly } from 'vitepress'
const windowWidth = ref(0)
onMounted(() => {
windowWidth.value = window.innerWidth
const handleResize = () => {
windowWidth.value = window.innerWidth
}
window.addEventListener('resize', handleResize)
return () => {
window.removeEventListener('resize', handleResize)
}
})
</script>
Environment Detection
Detect the environment safely:
// utils/environment.js
export const isClient = typeof window !== 'undefined'
export const isServer = !isClient
export function runOnClient(fn) {
if (isClient) {
return fn()
}
return null
}
export function runOnServer(fn) {
if (isServer) {
return fn()
}
return null
}
<script setup>
import { isClient, runOnClient } from '../utils/environment'
// Safe environment detection
const userAgent = runOnClient(() => navigator.userAgent) || 'Unknown'
const currentUrl = runOnClient(() => window.location.href) || ''
</script>
Handling External Libraries
Dynamic Imports
Load libraries dynamically on the client:
<script setup>
import { ref, onMounted } from 'vue'
const chart = ref(null)
const chartInstance = ref(null)
onMounted(async () => {
// Dynamic import prevents SSR issues
const { Chart } = await import('chart.js')
chartInstance.value = new Chart(chart.value, {
type: 'bar',
data: {
labels: ['A', 'B', 'C'],
datasets: [{
data: [1, 2, 3]
}]
}
})
})
</script>
<template>
<div>
<canvas ref="chart"></canvas>
</div>
</template>
Library Wrappers
Create SSR-safe wrappers for libraries:
<!-- components/SafeChart.vue -->
<template>
<div>
<canvas v-if="isClient" ref="chartCanvas"></canvas>
<div v-else class="chart-placeholder">
Chart loading...
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const props = defineProps({
data: Object,
options: Object
})
const isClient = ref(false)
const chartCanvas = ref(null)
let chartInstance = null
onMounted(async () => {
isClient.value = true
const { Chart } = await import('chart.js')
chartInstance = new Chart(chartCanvas.value, {
type: 'bar',
data: props.data,
options: props.options
})
})
onUnmounted(() => {
if (chartInstance) {
chartInstance.destroy()
}
})
</script>
Plugin Configuration
Configure Vite to handle problematic libraries:
// .vitepress/config.js
export default {
vite: {
ssr: {
// Externalize dependencies that cause SSR issues
external: ['some-browser-only-lib'],
// Transform specific modules
noExternal: ['lib-that-needs-transformation']
},
define: {
// Define globals for SSR compatibility
global: 'globalThis'
},
optimizeDeps: {
// Exclude from pre-bundling
exclude: ['browser-only-package']
}
}
}
Data Fetching Patterns
SSR-Safe Data Fetching
Fetch data that works in both environments:
<script setup>
import { ref, onMounted } from 'vue'
const data = ref(null)
const loading = ref(true)
const error = ref(null)
// This runs during SSR and provides initial data
const initialData = await fetchInitialData()
data.value = initialData
onMounted(async () => {
// This runs only on the client for additional data
try {
const additionalData = await fetchAdditionalData()
data.value = { ...data.value, ...additionalData }
} catch (err) {
error.value = err
} finally {
loading.value = false
}
})
async function fetchInitialData() {
// This should work in both server and client environments
const response = await fetch('/api/initial-data')
return response.json()
}
async function fetchAdditionalData() {
// This might use browser-specific APIs
const response = await fetch('/api/additional-data', {
headers: {
'User-Agent': navigator.userAgent
}
})
return response.json()
}
</script>
Using Data Loaders
Use VitePress data loaders for SSR-safe data:
// data/posts.data.js
export default {
async load() {
// This runs during build time (SSR)
const posts = await fetch('https://api.example.com/posts')
.then(res => res.json())
return posts.map(post => ({
title: post.title,
slug: post.slug,
excerpt: post.excerpt,
date: new Date(post.date)
}))
}
}
<script setup>
import { data as posts } from '../data/posts.data.js'
// This data is available during SSR
console.log('Posts loaded:', posts.length)
</script>
<template>
<div>
<article v-for="post in posts" :key="post.slug">
<h2>{{ post.title }}</h2>
<p>{{ post.excerpt }}</p>
</article>
</div>
</template>
State Management
SSR-Safe State
Handle state that works in both environments:
// stores/app.js
import { reactive, ref } from 'vue'
// SSR-safe initial state
const state = reactive({
user: null,
theme: 'light',
language: 'en'
})
// Client-only state
const clientState = ref(null)
export function useAppStore() {
// Initialize client-only state
if (typeof window !== 'undefined' && !clientState.value) {
clientState.value = {
windowWidth: window.innerWidth,
userAgent: navigator.userAgent,
online: navigator.onLine
}
}
return {
state,
clientState: clientState.value
}
}
Hydration Mismatches
Prevent hydration mismatches:
<script setup>
import { ref, onMounted } from 'vue'
const mounted = ref(false)
const currentTime = ref(new Date().toISOString())
onMounted(() => {
mounted.value = true
// Update time only after hydration
const updateTime = () => {
currentTime.value = new Date().toISOString()
}
const interval = setInterval(updateTime, 1000)
return () => clearInterval(interval)
})
</script>
<template>
<div>
<!-- Show static content during SSR -->
<p v-if="!mounted">Loading current time...</p>
<!-- Show dynamic content after hydration -->
<p v-else>Current time: {{ currentTime }}</p>
</div>
</template>
Testing SSR Compatibility
Build Testing
Test your build for SSR issues:
# Build the site
npm run docs:build
# Check for SSR errors in build output
npm run docs:build 2>&1 | grep -i error
# Preview the built site
npm run docs:preview
Component Testing
Test components for SSR compatibility:
// tests/ssr-compat.test.js
import { describe, it, expect } from 'vitest'
import { renderToString } from '@vue/server-renderer'
import { createApp } from 'vue'
import MyComponent from '../components/MyComponent.vue'
describe('SSR Compatibility', () => {
it('should render component without errors', async () => {
const app = createApp(MyComponent)
// This should not throw
const html = await renderToString(app)
expect(html).toContain('expected content')
})
it('should handle browser-only code gracefully', async () => {
// Mock browser environment
global.window = undefined
global.document = undefined
const app = createApp(MyComponent)
// Should not throw even without browser APIs
expect(async () => {
await renderToString(app)
}).not.toThrow()
})
})
Debugging SSR Issues
Common Error Messages
Understanding common SSR errors:
ReferenceError: window is not defined
ReferenceError: document is not defined
ReferenceError: localStorage is not defined
ReferenceError: navigator is not defined
Debug Techniques
Debug SSR issues effectively:
// Add debug logging
console.log('Environment:', typeof window !== 'undefined' ? 'client' : 'server')
// Use try-catch for problematic code
try {
const result = window.someAPI()
} catch (error) {
console.warn('Browser API not available:', error.message)
}
// Conditional execution
if (process.client) {
// Client-only code
} else if (process.server) {
// Server-only code
}
Build Analysis
Analyze build output for issues:
// .vitepress/config.js
export default {
vite: {
build: {
rollupOptions: {
onwarn(warning, warn) {
// Log SSR-related warnings
if (warning.code === 'EVAL' || warning.message.includes('window')) {
console.warn('Potential SSR issue:', warning.message)
}
warn(warning)
}
}
}
}
}
Best Practices
Code Organization
- Separate client and server logic
- Use composition functions for reusable SSR-safe code
- Keep browser-specific code in lifecycle hooks
- Use TypeScript for better error detection
Performance
- Minimize client-only code
- Use data loaders for build-time data fetching
- Implement proper loading states
- Optimize bundle splitting
Error Handling
- Always check for browser environment
- Provide fallbacks for failed operations
- Use try-catch for risky operations
- Log errors appropriately
Testing
- Test components in both environments
- Use automated SSR compatibility tests
- Monitor build output for warnings
- Test with JavaScript disabled