Files
stack/packages/mosaic/framework/skills/vitepress/references/advanced-ssr.md
T
fargo 1a822493ba format: apply repo prettier (3.8.1) to the folded skills tree
963 markdown files reformatted with the repository's pinned prettier so
pnpm format:check covers the folded tree like every other repo file.

The formatter's embedded-language pass also normalized code fences
(TS semicolons, closed HTML tags in examples, lowercased CSS hex colors,
one renumbered list that skipped an index). Alphanumeric token deltas vs
the fold commit were audited file-by-file; all are formatter-equivalent
markup normalizations plus the four sanitized skills.
2026-08-19 14:37:17 -05: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