Index of every page on this site: llms.txt
Svelte
Get Started

Installing Unhead with Svelte

On this page

Install @unhead/svelte, create a head with createHead(), and provide it through setContext(). Components can patch useHead() entries from $effect() for reactive updates.

Use Unhead when you need typed SEO input, structured data integration, or script loading beyond Svelte's built-in <svelte:head>. The examples follow Vite's SSR structure; a Vite SPA uses the same client setup.

Using SvelteKit? See the dedicated SvelteKit integration guide instead.

Demos

Setup

1. Install Unhead

Install the @unhead/svelte dependency in your project.

bash
pnpm add @unhead/svelte

2. Set Up Client-Side Rendering

Create the browser instance from @unhead/svelte/client. In a Vite SSR app this usually belongs in entry-client.ts; in an SPA, put it in the main entry.

src/entry-client.ts
import { createHead, UnheadContextKey } from '@unhead/svelte/client'
import { hydrate } from 'svelte'
import App from './App.svelte'
import './app.css'

// SSR or SPA usage

const unhead = createHead()
const context = new Map()
context.set(UnheadContextKey, unhead)

hydrate(App, {
  target: document.getElementById('app')!,
  context
})
src/main.ts
import { createHead, UnheadContextKey } from '@unhead/svelte/client'
import { mount } from 'svelte'
import App from './App.svelte'
import './app.css'

// SPA usage

const unhead = createHead()
const context = new Map()
context.set(UnheadContextKey, unhead)

const app = mount(App, {
  target: document.getElementById('app')!,
  context,
})

export default app

3. Set Up Server-Side Rendering

Serving your app as an SPA? You can skip this step. See the SPA guide for template defaults, crawler limits, and route prerendering.

For SSR, create a fresh instance from @unhead/svelte/server for each request and return it with Svelte's render result.

src/entry-server.ts
import { createHead, UnheadContextKey } from '@unhead/svelte/server'
import { render as _render } from 'svelte/server'
import App from './App.svelte'

export function render(_url: string) {
  const unhead = createHead()
  const context = new Map()
  context.set(UnheadContextKey, unhead)
  const rendered = _render(App, { context })
  return {
    ...rendered,
    unhead,
  }
}

After Svelte renders, pass the instance and complete HTML template to transformHtmlTemplate():

server.ts
import { transformHtmlTemplate } from '@unhead/svelte/server'
// ...

// Serve HTML
app.use('*all', async (req, res) => {
  try {
    // ...

    const rendered = await render(url)
    const html = transformHtmlTemplate(
      rendered.unhead,
      template
        .replace(`<!--app-head-->`, rendered.head ?? '')
        .replace(`<!--app-html-->`, rendered.body ?? '')
    )

    res.status(200).set({ 'Content-Type': 'text/html' }).send(html)
  }
  catch (e) {
    // ...
  }
})
// ..

4. Add Head Tags

The server head inserts three defaults:

  • <meta charset="utf-8">
  • <meta name="viewport" content="width=device-width, initial-scale=1">
  • <html lang="en">

Set server-only defaults through init. Keeping them in the server entry also keeps them out of the client bundle.

src/entry-server.ts
import { createHead } from '@unhead/svelte/server'

export function render(_url: string) {
  const head = createHead({
    // change default initial lang
    init: [
      {
        htmlAttrs: { lang: 'en' },
        title: 'Default title',
        titleTemplate: '%s - My Site',
      },
    ]
  })
  const html = `<!-- your html -->`
  return { html, head }
}

Use useHead() for the full head object or useSeoMeta() for flat SEO metadata.

App.svelte
<script lang="ts">
  import { useHead, useSeoMeta } from '@unhead/svelte'

  // a.
  useHead({
    title: 'Example Site',
    meta: [
      { name: 'description', content: 'An example page description.' }
    ]
  })

  // b.
  useSeoMeta({
    title: 'Example Site',
    description: 'An example page description.'
  })
</script>

For handling reactive input, check out the Reactivity guide.

5. Enable Auto-Imports (Optional)

unplugin-auto-import can provide the composables without explicit imports:

vite.config.ts
import { autoImports } from '@unhead/svelte'
import AutoImport from 'unplugin-auto-import/vite'

export default defineConfig({
  plugins: [
    AutoImport({
      imports: [
        autoImports,
      ],
    }),
    // ...
  ]
})

Next Steps

See reactive input before passing Svelte state to these composables:

Optional integrations:

Did this page help you?