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

Install Unhead in TypeScript Projects

Install unhead, create a head with createHead(), and call useHead() directly. SSR uses a separate server instance and renderSSRHead().

This setup is for TypeScript applications without a framework adapter. If you use a framework, select its adapter below.

Demos

Install Unhead

Install the unhead dependency:

bash
pnpm add unhead

Client-Side Rendering

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

entry-client.ts
import { createHead } from 'unhead/client'
import { setupCounter } from './counter'
import './style.css'
import './typescript.svg'

declare global {
  interface Window {
    __UNHEAD__: ReturnType<typeof createHead>
  }
}

window.__UNHEAD__ = createHead()

setupCounter(document.querySelector('#counter') as HTMLButtonElement)

This example uses window.__UNHEAD__ to keep the first setup short. For application code, wrap the composables in your own context instead of relying on a global.

Follow the Wrapping Composables guide for the recommended way to handle context.

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.

Somewhere in your server entry, create a server head instance.

main.ts
import { createHead } from 'unhead/server'
export function render(_url: string) {
  const head = createHead()
  const html = `<!-- your html -->`
  return { html, head }
}

After rendering the application, pass the head instance and complete template to transformHtmlTemplate():

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

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

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

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

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.

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

This counter creates one entry, then patches it on each click:

counter.ts
import { useHead } from 'unhead'

export function setupCounter(element: HTMLButtonElement) {
  let counter = 0
  const entry = useHead(window.__UNHEAD__, {})
  const setCounter = (count: number) => {
    counter = count
    element.textContent = `count is ${counter}`
    entry.patch({
      title: counter ? `count is ${counter}` : null,
    })
  }
  element.addEventListener('click', () => setCounter(counter + 1))
  setCounter(0)
}

Next Steps

  1. Learn more about app context in the Wrapping Composables guide
  2. Consider using the Vite plugin
Did this page help you?