Index of every page on this site: llms.txt
Solid.js
Core Concepts

Streaming SSR

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(). 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.

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.

Setup

Vite Plugin

Enable streaming transforms:

// 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().

Pass each response's CSP nonce to createStreamableHead({ nonce }). Unhead applies it to its bootstrap and patch scripts. Use the same nonce in the response CSP header and application scripts. Pass it to renderToStream() too, as shown below. See the core CSP example for request setup.

Server Entry

// 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, nonce?: string) {
  const { head, onCompleteShell, wrapStream } = createStreamableHead({ nonce })

  const { readable, writable } = new TransformStream()

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

  return wrapStream(readable, template)
}

Reuse a Production Template

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

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

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.

Unhead parses raw templates on each request. If production HTML stays unchanged, reuse a prepareTemplate() result. Each request still resolves head tags. Prepared templates do not cache rendered head output.

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

Client Entry

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

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.

Did this page help you?