---
title: "Using Unhead with SvelteKit"
description: "Set up Unhead in SvelteKit using hooks.server.ts and +layout.svelte. Full SSR support with transformHtmlTemplate()."
canonical_url: "https://unhead.unjs.io/docs/svelte/head/guides/get-started/sveltekit"
last_updated: "2026-06-30T06:50:53.549Z"
---

## Introduction

[SvelteKit](https://kit.svelte.dev/) provides its own SSR infrastructure that differs from the basic Vite SSR template. This guide covers how to wire up Unhead using SvelteKit's `handle` hook and Svelte context API.

**Quick Start:** Install `@unhead/svelte`, create the head in `hooks.server.ts`, set SSR tags via `locals.unhead.push()` in load functions, provide a client-side head via context in `+layout.svelte`, and use `useHead()` in your components for client-side reactivity.

## 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, {
    transformPageChunk: async ({ html }) => {
      return transformHtmlTemplate(unhead, html)
    },
  })

  return response
}
```

`transformHtmlTemplate()` parses the HTML to extract any existing head tags and attributes, pushes them into Unhead, then renders all managed tags back into the document — replacing and deduplicating as needed. It does not rely on static placeholder tokens.

### 4. Set SSR head tags in server load functions

SvelteKit serialises `load` return values, so the `unhead` instance cannot be passed to components directly. Instead, push SSR head tags from your server load functions using `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

After hydration, create a client-side head instance and provide it via Svelte context so that `useHead()` works in components for client-side navigation and reactivity:

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

After hydration, `useHead()` and `useSeoMeta()` are available in any component for client-side head management:

```svelte [src/routes/+page.svelte]
<script lang="ts">
  import { useHead } from '@unhead/svelte'

  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 type { PageData } from './$types'

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

  const entry = useSeoMeta()

  $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, {
    transformPageChunk: async ({ html }) => {
      return transformHtmlTemplate(unhead, html)
    },
  })

  return response
}
```

## 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, renders tags into HTML 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>

On the **server**, `hooks.server.ts` creates the head instance. Server load functions push tags via `locals.unhead`. After rendering, `transformHtmlTemplate()` extracts existing head markup from the HTML, merges it with managed tags, and writes the final head into the response.

On the **client**, the layout creates a new head instance and provides it via context. SvelteKit hydrates the page and subsequent navigation triggers reactive updates through `useHead()` and `$effect()`.

## 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 optimized third-party script loading
- Read the [Reactivity](/docs/svelte/head/guides/core-concepts/reactivity) guide for reactive head tags
