Skip to content

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:

  1. Build Time: Components are rendered on the server
  2. Static Generation: HTML files are generated with pre-rendered content
  3. Hydration: Client-side JavaScript takes over for interactivity

Common SSR Issues

Browser-Only APIs

Code that relies on browser APIs will fail during SSR:

javascript
// ❌ 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:

vue
<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:

javascript
// ❌ 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:

vue
<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:

vue
<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:

javascript
// 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
}
vue
<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:

vue
<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:

vue
<!-- 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:

javascript
// .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:

vue
<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:

javascript
// 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)
    }))
  }
}
vue
<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:

javascript
// 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:

vue
<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:

bash
# 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:

javascript
// 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:

javascript
// 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:

javascript
// .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

VitePress Development Guide