Files
agent-skills/skills/vitepress/references/advanced-ssr.md
Jason Woltje f5792c40be feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:27:42 -06:00

229 lines
4.0 KiB
Markdown

---
name: vitepress-ssr-compatibility
description: Server-side rendering compatibility, ClientOnly component, and handling browser-only code
---
# SSR Compatibility
VitePress pre-renders pages on the server during build. All Vue code must be SSR-compatible.
## The Rule
Only access browser/DOM APIs in Vue lifecycle hooks:
- `onMounted()`
- `onBeforeMount()`
```vue
<script setup>
import { onMounted, ref } from 'vue'
const windowWidth = ref(0)
onMounted(() => {
// Safe - runs only in browser
windowWidth.value = window.innerWidth
})
</script>
```
**Do NOT** access at top level:
```vue
<script setup>
// WRONG - runs during SSR where window doesn't exist
const width = window.innerWidth
</script>
```
## ClientOnly Component
Wrap non-SSR-friendly components:
```vue
<template>
<ClientOnly>
<BrowserOnlyComponent />
</ClientOnly>
</template>
```
## Libraries That Access Browser on Import
Some libraries access `window` or `document` when imported:
### Dynamic Import in onMounted
```vue
<script setup>
import { onMounted } from 'vue'
onMounted(async () => {
const lib = await import('browser-only-library')
lib.doSomething()
})
</script>
```
### Conditional Import
```ts
if (!import.meta.env.SSR) {
const lib = await import('browser-only-library')
lib.doSomething()
}
```
### In enhanceApp
```ts
// .vitepress/theme/index.ts
export default {
async enhanceApp({ app }) {
if (!import.meta.env.SSR) {
const plugin = await import('browser-plugin')
app.use(plugin.default)
}
}
}
```
## defineClientComponent
Helper for components that access browser on import:
```vue
<script setup>
import { defineClientComponent } from 'vitepress'
const BrowserComponent = defineClientComponent(() => {
return import('browser-only-component')
})
</script>
<template>
<BrowserComponent />
</template>
```
With props and slots:
```vue
<script setup>
import { ref, h } from 'vue'
import { defineClientComponent } from 'vitepress'
const componentRef = ref(null)
const BrowserComponent = defineClientComponent(
() => import('browser-only-component'),
// Props passed to h()
[
{ ref: componentRef, someProp: 'value' },
{
default: () => 'Default slot content',
header: () => h('div', 'Header slot')
}
],
// Callback after component loads
() => {
console.log('Component loaded', componentRef.value)
}
)
</script>
```
## Teleports
Teleport to body only with SSG:
```vue
<ClientOnly>
<Teleport to="body">
<div class="modal">Modal content</div>
</Teleport>
</ClientOnly>
```
For other targets, use `postRender` hook:
```ts
// .vitepress/config.ts
export default {
async postRender(context) {
// Inject teleport content into final HTML
}
}
```
## Common SSR Errors
### "window is not defined"
Code accesses `window` at module level:
```ts
// BAD
const width = window.innerWidth
// GOOD
let width: number
onMounted(() => {
width = window.innerWidth
})
```
### "document is not defined"
Same issue with `document`:
```ts
// BAD
const el = document.querySelector('#app')
// GOOD
onMounted(() => {
const el = document.querySelector('#app')
})
```
### Hydration Mismatch
Server and client render different content:
```vue
<!-- BAD - different on server vs client -->
<div>{{ typeof window !== 'undefined' ? 'client' : 'server' }}</div>
<!-- GOOD - consistent -->
<ClientOnly>
<div>Client only content</div>
</ClientOnly>
```
## Checking Environment
```ts
// In Vue component
import.meta.env.SSR // true on server, false on client
// In VitePress
import { inBrowser } from 'vitepress'
if (inBrowser) {
// Client-only code
}
```
## Key Points
- Access browser APIs only in `onMounted` or `onBeforeMount`
- Use `<ClientOnly>` for non-SSR components
- Use `defineClientComponent` for libraries that access browser on import
- Check `import.meta.env.SSR` for environment-specific code
- Teleport to body only, or use `postRender` hook
- Consistent rendering prevents hydration mismatches
<!--
Source references:
- https://vitepress.dev/guide/ssr-compat
-->