---
title: "Using Unhead with SvelteKit"
description: "Set up Unhead in SvelteKit using hooks.server.ts, server load functions, and +layout.svelte."
canonical_url: "https://unhead.unjs.io/docs/svelte/head/guides/get-started/sveltekit"
last_updated: "2026-08-11T00:46:06.972Z"
---

[SvelteKit](https://svelte.dev/docs/kit) applications need separate Unhead instances for server rendering and client-side navigation. This guide connects them through SvelteKit's [`handle` hook](https://svelte.dev/docs/kit/hooks#Server-hooks-handle), `locals`, and Svelte context.

## Setup

### 1. Install the package

<module-install name="@unhead/svelte@next">



</module-install>

### 2. Update app.d.ts

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

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

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

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

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

```svelte [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 />
```

<note>

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.

</note>

### 6. Use useHead() in your components

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

```svelte [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:

```svelte [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>
```

<note>

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.

</note>

### 7. Add default tags (optional)

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

```ts [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

<table>
<thead>
  <tr>
    <th>
      File
    </th>
    
    <th>
      Role
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        hooks.server.ts
      </code>
    </td>
    
    <td>
      Creates a per-request head instance, buffers HTML responses, then renders tags via <code>
        transformHtmlTemplate()
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        +layout.server.ts
      </code>
      
       / <code>
        +page.server.ts
      </code>
    </td>
    
    <td>
      Push SSR head tags via <code>
        locals.unhead.push()
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        +layout.svelte
      </code>
    </td>
    
    <td>
      Provides a client-side head instance via Svelte context
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        +page.svelte
      </code>
      
       / any component
    </td>
    
    <td>
      Calls <code>
        useHead()
      </code>
      
       / <code>
        useSeoMeta()
      </code>
      
       for client-side reactivity
    </td>
  </tr>
</tbody>
</table>

## Next Steps

- Use [`useSeoMeta()`](/docs/head/api/composables/use-seo-meta) for a flat, type-safe SEO API
- Add [`useSchemaOrg()`](/docs/schema-org/api/composables/use-schema-org) for structured data
- Use [`useScript()`](/docs/head/api/composables/use-script) for third-party script loading, triggers, and lifecycle hooks
- Read the [Reactivity](/docs/svelte/head/guides/core-concepts/reactivity) guide for reactive head tags
