---
title: "Streaming SSR"
description: "Stream head tags as Suspense boundaries resolve during Solid.js SSR"
canonical_url: "https://unhead.unjs.io/docs/solid-js/head/guides/core-concepts/streaming"
last_updated: "2026-08-31T04:39:05.394Z"
---

Solid can send the document shell while resources inside `<Suspense>` are still loading.
Unhead sends their later head entries with the resolved content.

This guide uses Solid's [`renderToStream()`](https://docs.solidjs.com/reference/rendering/render-to-stream).
The example connects `pipeTo()` to a Web `TransformStream`.

## How It Works

1. **Shell:** Solid calls `onCompleteShell()` and Unhead captures the initial tags.
2. **Suspense:** A resource settles and its component calls `useHead()`.
3. **Patch:** `HeadStream` serializes that entry into the Solid stream.
4. **Hydration:** The client instance takes over the streamed state.

## Where Tags Render

`onCompleteShell` marks Solid's initial shell.
Entries registered before this callback reach the initial head.

Entries from later resource content follow two paths:

- Head-only tags become inline patch scripts.
- JSON-LD, `noscript`, and body-positioned tags become Streamed Body Tags before `</body>`.

The build plugin adds `HeadStream` to transformed components that use head composables.
Keep the plugin enabled or late entries will not reach the response.

<warning>

Many bots and link previews do not run patch scripts.
Register canonical links, robots, descriptions, and Open Graph tags before the shell.

Google can read [JSON-LD from the head or body](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data#supported-formats).

</warning>

## Setup

### Vite Plugin

Enable streaming transforms:

```ts
// vite.config.ts
import { Unhead } from '@unhead/solid-js/vite'
import solid from 'vite-plugin-solid'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    solid(),
    Unhead({ streaming: true }),
  ],
})
```

For webpack projects, import `Unhead` from `@unhead/solid-js/bundler` and call `Unhead({ streaming: true }).webpack()`.

<warning>

The automatic wrapper does not add CSP nonces to its inline scripts.
For a nonce-only policy, use the [manual core flow](/docs/typescript/head/guides/core-concepts/streaming#template-free-integration).
Otherwise, use standard SSR.

</warning>

### Server Entry

```tsx
// entry-server.tsx
import { renderToStream } from 'solid-js/web'
import { createStreamableHead, type PreparedTemplate, UnheadContext } from '@unhead/solid-js/stream/server'
import App from './App'

export function render(url: string, template: string | PreparedTemplate) {
  const { head, onCompleteShell, wrapStream } = createStreamableHead()

  const { readable, writable } = new TransformStream()

  renderToStream(() => (
    <UnheadContext.Provider value={head}>
      <App url={url} />
    </UnheadContext.Provider>
  ), { onCompleteShell }).pipeTo(writable)

  return wrapStream(readable, template)
}
```

### Reuse a Production Template

<note>

`prepareTemplate()` and `PreparedTemplate` are experimental. Their API may change in a future minor release.

</note>

If `index.html` stays unchanged, prepare it once before calling `render()`:

```ts
import { readFile } from 'node:fs/promises'
import { prepareTemplate } from '@unhead/solid-js/stream/server'

const template = prepareTemplate(await readFile('dist/client/index.html', 'utf8'))
```

Keep `template` at process scope. Create a new `head` for every request.
Sharing `head` can expose one request's tags to another.

In development, prepare the result of `transformIndexHtml()` for each request.
Prepare again after any HTML change.

### Client Entry

```tsx
// entry-client.tsx
import { hydrate } from 'solid-js/web'
import { createStreamableHead, UnheadContext } from '@unhead/solid-js/stream/client'
import App from './App'

const head = createStreamableHead()
if (!head)
  throw new Error('Unhead streaming bootstrap state is missing')

hydrate(() => (
  <UnheadContext.Provider value={head}>
    <App url={window.location.pathname} />
  </UnheadContext.Provider>
), document.getElementById('app')!)
```

If this returns `undefined`, the response has no stream bootstrap.
Check the server renderer and HTML template.

## Usage

With the Vite plugin enabled, call `useHead()` in the component that reads the resource:

```tsx
import { Suspense, createResource } from 'solid-js'
import { useHead } from '@unhead/solid-js'

function AsyncPage() {
  const [data] = createResource(fetchPageData)

  useHead({
    get title() { return data()?.title },
    meta: [
      { name: 'description', get content() { return data()?.description } }
    ]
  })

  return <div>{data()?.content}</div>
}

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <AsyncPage />
    </Suspense>
  )
}
```

## When to Skip

If you have no async resources, use standard SSR.
