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:

CauseExample
Browser-only API in template{{ window.innerWidth }}
Time/locale-dependent values{{ new Date().toLocaleString() }}
Browser extensions modifying HTMLAd 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.

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

Use the #fallback slot to show placeholder content during SSR/SSG:

vue
<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.

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

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

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

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

vue
<!-- 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> or defineClientComponent.
  • Never access window, document, or navigator at the top level of a <script setup> block — move it into onMounted.
  • 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.SSR for conditional side effects in composables.

Contributors