Svelte
Get Started

Using Unhead with SvelteKit

SvelteKit applications need separate Unhead instances for server rendering and client-side navigation. This guide connects them through SvelteKit's handle hook, locals, and Svelte context.

Setup

1. Install the package

bash
pnpm add @unhead/svelte@next

2. Update app.d.ts

Extend SvelteKit's Locals interface so the head instance can flow through the request:

src/app.d.ts
import type { Unhead } from '@unhead/svelte/server'

declare global {
  namespace App {
    interface Locals {
      unhead: Unhead
    }
  }
}

export {}

3. Create the head in hooks.server.ts

Use the handle hook to create a head instance per request and render the managed tags into the HTML response:

src/hooks.server.ts
import { createHead, transformHtmlTemplate } from '@unhead/svelte/server'
import type { Handle } from '@sveltejs/kit'

export const handle: Handle = async ({ event, resolve }) => {
  const unhead = createHead()
  event.locals.unhead = unhead

  const response = await resolve(event)

  if (!response.headers.get('content-type')?.includes('text/html'))
    return response

  const html = await response.text()
  const transformed = transformHtmlTemplate(unhead, html)
  const headers = new Headers(response.headers)
  headers.delete('content-length')

  return new Response(transformed, {
    headers,
    status: response.status,
    statusText: response.statusText,
  })
}

transformHtmlTemplate() needs the complete HTML document, so this example buffers HTML responses instead of using SvelteKit's chunk transform. It extracts existing head tags and attributes, merges the managed tags, and writes the result back into the document. Non-HTML responses pass through unchanged.

4. Set SSR head tags in server load functions

SvelteKit serializes load return values, so you cannot pass the unhead instance directly to components. Push SSR head tags from server load functions through locals.unhead:

src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types'

export const load: LayoutServerLoad = async ({ locals }) => {
  // Set site-wide SSR head tags
  locals.unhead.push({
    htmlAttrs: { lang: 'en' },
    titleTemplate: '%s | My Site',
  })
}

Per-page SSR head tags work the same way:

src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types'

export const load: PageServerLoad = async ({ locals, params }) => {
  const post = await getPost(params.slug)

  // Push page-specific head tags for SSR
  locals.unhead.push({
    title: post.title,
    meta: [
      { name: 'description', content: post.excerpt },
      { property: 'og:image', content: post.coverImage },
    ],
  })

  return { post }
}

These tags are rendered into the HTML by transformHtmlTemplate() in hooks.server.ts before the response is sent.

5. Provide a client-side head in +layout.svelte

Create a client-side head instance in the root layout and provide it through Svelte context. Components can then call useHead() during client-side navigation:

src/routes/+layout.svelte
<script lang="ts">
  import { setContext } from 'svelte'
  import { browser } from '$app/environment'
  import { UnheadContextKey } from '@unhead/svelte'
  import { createHead } from '@unhead/svelte/client'

  if (browser) {
    const unhead = createHead()
    setContext(UnheadContextKey, unhead)
  }
</script>

<slot />
On the server, head tags are managed via locals.unhead.push() in load functions. The layout only needs to provide the client-side instance so that useHead() calls work after hydration for client-side navigation.

6. Use useHead() in your components

Guard component composables with browser because the layout provides this head instance only on the client:

src/routes/+page.svelte
<script lang="ts">
  import { useHead } from '@unhead/svelte'
  import { browser } from '$app/environment'

  if (browser) {
    useHead({
      title: 'My SvelteKit App',
      meta: [
        { name: 'description', content: 'Built with SvelteKit and Unhead' }
      ]
    })
  }
</script>

<h1>Home</h1>

For reactive tags driven by page data during client-side navigation:

src/routes/blog/[slug]/+page.svelte
<script lang="ts">
  import { useSeoMeta } from '@unhead/svelte'
  import { browser } from '$app/environment'
  import type { PageData } from './$types'

  let { data }: { data: PageData } = $props()

  const entry = browser ? useSeoMeta() : undefined

  $effect(() => {
    entry?.patch({
      title: data.post.title,
      description: data.post.excerpt,
      ogImage: data.post.coverImage,
    })
  })
</script>
For initial page loads (SSR), set head tags in +page.server.ts via locals.unhead.push(). The useHead() composable in components handles client-side navigation and reactive updates after hydration.

7. Add default tags (optional)

Set site-wide defaults in hooks.server.ts using the init option:

src/hooks.server.ts
import { createHead, transformHtmlTemplate } from '@unhead/svelte/server'
import type { Handle } from '@sveltejs/kit'

export const handle: Handle = async ({ event, resolve }) => {
  const unhead = createHead({
    init: [
      {
        htmlAttrs: { lang: 'en' },
        title: 'My SvelteKit App',
        titleTemplate: '%s | My Site',
        meta: [
          { name: 'description', content: 'Default site description' }
        ],
      },
    ],
  })
  event.locals.unhead = unhead

  const response = await resolve(event)

  if (!response.headers.get('content-type')?.includes('text/html'))
    return response

  const html = await response.text()
  const transformed = transformHtmlTemplate(unhead, html)
  const headers = new Headers(response.headers)
  headers.delete('content-length')

  return new Response(transformed, {
    headers,
    status: response.status,
    statusText: response.statusText,
  })
}

How the pieces fit together

FileRole
hooks.server.tsCreates a per-request head instance, buffers HTML responses, then renders tags via transformHtmlTemplate()
+layout.server.ts / +page.server.tsPush SSR head tags via locals.unhead.push()
+layout.svelteProvides a client-side head instance via Svelte context
+page.svelte / any componentCalls useHead() / useSeoMeta() for client-side reactivity

Next Steps

Did this page help you?