SSR Compatibility
SSR Compatibility
Valaxy builds your site using SSG (Static Site Generation), which renders pages to HTML at build time via Vue’s server-side rendering (SSR). This means components run in a Node.js environment during the build, where browser APIs like window, document, and navigator are not available.
Upgrading from the old `vite-ssg` engine
Valaxy used to ship a JSDOM-based vite-ssg engine, removed in v1.0 (see #706). JSDOM silently provided window, document, and navigator during SSR, so code that touched these globals at render time appeared to "work". The Valaxy SSG engine renders pure strings with no DOM, so the same code now throws or hydrates incorrectly.
If you are upgrading and a theme/addon relied on a DOM during SSR, guard every browser-only access with the patterns below.
Why Hydration Mismatches Happen
After SSG generates static HTML, Vue "hydrates" it in the browser — attaching event listeners and making it interactive. If the HTML rendered on the server differs from what the client renders, you get a hydration mismatch warning.
Common causes:
| Cause | Example |
|---|---|
| Browser-only API in template | {{ window.innerWidth }} |
| Time/locale-dependent values | {{ new Date().toLocaleString() }} |
| Browser extensions modifying HTML | Ad blockers injecting elements |
| Non-standard HTML nesting | <p> inside <p>, <div> inside <a> |
<ClientOnly>
Wrap browser-only content with the built-in <ClientOnly> component. Its content is only rendered on the client side.
<template>
<ClientOnly>
<BrowserOnlyComponent />
</ClientOnly>
</template>Use the #fallback slot to show placeholder content during SSR/SSG:
<template>
<ClientOnly>
<HeavyChart :data="chartData" />
<template #fallback>
<div class="chart-placeholder">
Loading chart...
</div>
</template>
</ClientOnly>
</template>defineClientComponent
For third-party libraries that access browser APIs at import time (not just at render time), use defineClientComponent. It delays the import() until the component mounts in the browser.
<script setup>
import { defineClientComponent } from 'valaxy'
const MyBrowserLib = defineClientComponent(
() => import('some-browser-only-lib')
)
</script>
<template>
<MyBrowserLib />
</template>You can pass props and a callback:
<script setup>
import { defineClientComponent } from 'valaxy'
const EchartsChart = defineClientComponent(
() => import('vue-echarts'),
[
{ option: chartOption, autoresize: true }, // props
{ default: () => h('div', 'Loading...') }, // children/slots
],
(mod) => {
// called after the module is loaded
console.log('vue-echarts loaded', mod)
},
)
</script>
<template>
<EchartsChart />
</template>onMounted + ref Pattern
For simple cases where you need browser APIs in logic (not in third-party imports), use Vue’s onMounted:
<script setup>
import { onMounted, ref } from 'vue'
const screenWidth = ref(0)
onMounted(() => {
screenWidth.value = window.innerWidth
})
</script>
<template>
<p>Screen width: {{ screenWidth }}</p>
</template>import.meta.env.SSR
Use the import.meta.env.SSR flag (provided by Vite) to conditionally execute code:
if (!import.meta.env.SSR) {
// This code only runs in the browser
document.addEventListener('scroll', handleScroll)
}This is useful in composables or setup functions where you need to guard browser-only side effects.
CSS-Based Responsive Rendering
Avoid using v-if with reactive viewport values for responsive layouts — this causes hydration mismatches because the server cannot know the viewport size. Use CSS instead:
<!-- Bad: causes hydration mismatch -->
<template>
<MobileNav v-if="isMobile" />
<DesktopNav v-else />
</template>
<!-- Good: use CSS media queries -->
<template>
<MobileNav class="mobile-only" />
<DesktopNav class="desktop-only" />
</template>
<style>
.mobile-only {
display: block;
}
.desktop-only {
display: none;
}
@media (min-width: 768px) {
.mobile-only {
display: none;
}
.desktop-only {
display: block;
}
}
</style>Tips for Theme & Addon Developers
- Always test with
pnpm demo:build(SSG build) —pnpm demo(dev mode) won’t catch SSR issues. - Wrap all browser-only third-party components with
<ClientOnly>ordefineClientComponent. - Never access
window,document, ornavigatorat the top level of a<script setup>block — move it intoonMounted. - If a library provides a server-safe build (e.g.,
import lib from 'lib/dist/ssr'), prefer that over wrapping with<ClientOnly>. - Use
import.meta.env.SSRfor conditional side effects in composables.