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

4.0 KiB

name, description
name description
vitepress-ssr-compatibility 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()
<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:

<script setup>
// WRONG - runs during SSR where window doesn't exist
const width = window.innerWidth
</script>

ClientOnly Component

Wrap non-SSR-friendly components:

<template>
  <ClientOnly>
    <BrowserOnlyComponent />
  </ClientOnly>
</template>

Libraries That Access Browser on Import

Some libraries access window or document when imported:

Dynamic Import in onMounted

<script setup>
import { onMounted } from 'vue'

onMounted(async () => {
  const lib = await import('browser-only-library')
  lib.doSomething()
})
</script>

Conditional Import

if (!import.meta.env.SSR) {
  const lib = await import('browser-only-library')
  lib.doSomething()
}

In enhanceApp

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

<script setup>
import { defineClientComponent } from 'vitepress'

const BrowserComponent = defineClientComponent(() => {
  return import('browser-only-component')
})
</script>

<template>
  <BrowserComponent />
</template>

With props and slots:

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

<ClientOnly>
  <Teleport to="body">
    <div class="modal">Modal content</div>
  </Teleport>
</ClientOnly>

For other targets, use postRender hook:

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

// BAD
const width = window.innerWidth

// GOOD
let width: number
onMounted(() => {
  width = window.innerWidth
})

"document is not defined"

Same issue with document:

// BAD
const el = document.querySelector('#app')

// GOOD
onMounted(() => {
  const el = document.querySelector('#app')
})

Hydration Mismatch

Server and client render different content:

<!-- BAD - different on server vs client -->
<div>{{ typeof window !== 'undefined' ? 'client' : 'server' }}</div>

<!-- GOOD - consistent -->
<ClientOnly>
  <div>Client only content</div>
</ClientOnly>

Checking Environment

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