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.
4.0 KiB
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
onMountedoronBeforeMount - Use
<ClientOnly>for non-SSR components - Use
defineClientComponentfor libraries that access browser on import - Check
import.meta.env.SSRfor environment-specific code - Teleport to body only, or use
postRenderhook - Consistent rendering prevents hydration mismatches