Skip to content

ServerOnly

olegivanov edited this page May 18, 2026 · 1 revision

ServerOnly

1. Overview

  • Name: ServerOnly
  • Purpose: Render children on the server and remove them after hydration on the client. The symmetric inverse of ClientOnly.
  • When to use: SEO meta strips, server-only progressive-enhancement content, no-JS fallbacks, or any tree that should appear in the initial HTML but should disappear once the SPA takes over.
  • Availability: All 5 framework adapters — @real-router/react/ssr, @real-router/preact/ssr, @real-router/solid/ssr, @real-router/vue/ssr, @real-router/svelte/ssr. Semantics identical across adapters.

2. Import and Basic Usage

<script lang="ts">
  import { ServerOnly } from "@real-router/svelte/ssr";

  import SeoMetaStrip from "./SeoMetaStrip.svelte";
</script>

<ServerOnly>
  {#snippet children()}
    <SeoMetaStrip />
  {/snippet}
</ServerOnly>

On the server, <SeoMetaStrip /> is part of the HTML. After hydration on the client, an $effect flips mounted = true and the content is removed (or replaced by fallback if provided).

3. Props

Prop Type Required Default Description
children Snippet Yes Snippet rendered on the server and during hydration; removed once $effect fires.
fallback Snippet No undefined Optional snippet that REPLACES children after hydration. Omit to render nothing on the client.

4. Behavior

Implementation (mirror of ClientOnly)

<script lang="ts">
  let mounted = $state(false);

  $effect(() => {
    mounted = true;
  });
</script>

{#if !mounted}
  {@render children()}
{:else if fallback}
  {@render fallback()}
{/if}
  • Server render: mounted = false → emits children.
  • Initial client render (pre-effect): mounted = false → matches server HTML during hydration.
  • Post-effect: mounted = true → swaps to fallback or renders nothing.

Hydration is non-destructive

Svelte 5's hydration walker accepts the {#if !mounted} branch matching during the initial pass. After $effect fires, the branch flip is treated as a normal reactive update (DOM nodes are removed/replaced), not a hydration mismatch.

SEO use case

This is the standard pattern for "show meta info in SSR HTML but don't keep it in the interactive DOM":

  • Search crawlers see <SeoMetaStrip> in the initial HTML.
  • Browsers that hydrate normally see it briefly, then it disappears.
  • No-JS browsers see it permanently (since $effect never runs without JS).

Progressive enhancement

<ServerOnly> doubles as a no-JS fallback. Anything inside children works without client hydration:

<ServerOnly>
  {#snippet children()}
    <noscript>
      <form action="/login" method="POST">
        <!-- Pure-HTML form for users without JS -->
      </form>
    </noscript>
  {/snippet}
</ServerOnly>

5. Examples

SEO meta strip

<script lang="ts">
  import { ServerOnly } from "@real-router/svelte/ssr";

  import type { Product } from "./types";

  export let product: Product;
</script>

<ServerOnly>
  {#snippet children()}
    <div class="sr-only" aria-hidden="true">
      <h1>{product.title}</h1>
      <p>{product.description}</p>
      <span>Price: {product.price}</span>
      <ul>
        {#each product.tags as tag}
          <li>{tag}</li>
        {/each}
      </ul>
    </div>
  {/snippet}
</ServerOnly>

With fallback — swap to a lightweight client variant

<ServerOnly>
  {#snippet children()}
    <article>
      <h1>{title}</h1>
      <p>{fullBody}</p>
    </article>
  {/snippet}
  {#snippet fallback()}
    <article>
      <h1>{title}</h1>
      <p>{truncatedBody}</p>
      <button onclick={expand}>Read more</button>
    </article>
  {/snippet}
</ServerOnly>

The server emits the full content (good for SEO); the client swaps to a truncated interactive variant.

Pair with <ClientOnly>

<!-- SEO-friendly server content -->
<ServerOnly>
  {#snippet children()}
    <SeoMetaStrip />
  {/snippet}
</ServerOnly>

<!-- Browser-only interactive content -->
<ClientOnly>
  {#snippet children()}
    <InteractiveMap />
  {/snippet}
</ClientOnly>

6. Gotchas

  • Brief flash during hydration. children appears in the HTML and stays until $effect runs (post-hydration tick). If the swap is visually noticeable, use a matching fallback to keep layout stable.
  • children is the SSR content, NOT a placeholder. Different mental model from <ClientOnly>: there children is the desired client UI; here children is the desired server UI.
  • No-JS users see children forever. Often desired (progressive enhancement). If you need different behavior, add a <noscript> block inside children.
  • $effect doesn't run on the server. Don't put server-side logic in $effect and expect it to fire — the rune is a client-side primitive.
  • Hydration warnings. Svelte 5 generally tolerates the {#if !mounted} flip. If your tree has client-only reactive state that conflicts with the server-rendered children, isolate that state inside the fallback branch.

7. Dependencies

  • No <RouterProvider> requirement.
  • Requires Svelte 5.x for runes.

8. Related

Navigation

Home


Concepts


Getting Started


Router Methods

Lifecycle

Navigation

State

URL & Path

Events


Standalone API

Tree-shakeable functions — import only what you need.

Routes — getRoutesApi(router)

Dependencies — getDependenciesApi(router)

Guards — getLifecycleApi(router)

Plugin Infrastructure — getPluginApi(router)

For plugin authors, not for general use.

SSR / SSG


React / Preact / Solid / Vue / Svelte Integration

Provider

Hooks

Components

SSR Components & Hooks

SSR-feature subpath — @real-router/{adapter}/ssr. Symmetric across React/Preact/Solid/Vue/Svelte.

  • Lazy (Svelte only — dynamic component import)
  • Await — read deferred SSR data by key
  • Streamed — Suspense-style boundary
  • ClientOnly — server fallback → client children swap
  • ServerOnly — server children, removed after hydration
  • HttpStatusCode — render-time HTTP status declaration
  • HttpStatusProvider — provides sink to descendant <HttpStatusCode>
  • useDeferred — read deferred Promise by key

DOM Utilities

Patterns


Subscription Layer (@real-router/sources)


Reactive Streams (@real-router/rx)


Plugins

Browser Plugin

Navigation Plugin

Hash Plugin

Memory Plugin

Lifecycle Plugin

Preload Plugin

Logger Plugin

Persistent Params

SSR Data

RSC Server

Validation

Search Schema

Utilities


Reference

Types

Error Codes

Clone this wiki locally