Unhead

How it works

Learn how Unhead works under the hood.

Core

The Unhead core is a small API which abstracts the common logic for all head integrations. It is designed to be used by any head integration, and is not specific to any one framework.

Extensibility and customisation is a core goal of Unhead. All internal logic is powered by hooks and plugins, hooks being provided by hookable.

The API is as follows:

export interface Unhead<Input extends {} = Head> {
  /**
   * The active head entries.
   */
  headEntries: () => HeadEntry<Input>[]
  /**
   * Create a new head entry.
   */
  push: (entry: Input, options?: HeadEntryOptions) => ActiveHeadEntry<Input>
  /**
   * Resolve tags from head entries.
   */
  resolveTags: () => Promise<HeadTag[]>
  /**
   * Exposed hooks for easier extension.
   */
  hooks: Hookable<HeadHooks>
  /**
   * Resolved options
   */
  resolvedOptions: CreateHeadOptions
  /**
   * @internal
   */
  _popSideEffectQueue: () => SideEffectsRecord
}

Head entries

Head entries are the data for Unhead. They are created by calling push on the Unhead instance.

An entry is a simple object which contains the tags to be added. Head entries are resolved to tags with the resolveTags method.

const myFirstEntry = head.push(
  {
    title: 'My title',
    meta: [
      {
        name: 'description',
        content: 'My description',
      },
    ],
  }
)

Resolve Tags

When a DOM or SSR render is triggered, the tags will be resolved.

This is done by calling resolveTags on the Unhead instance. This will return a promise which resolves to an array of tags.

const tags = await head.resolveTags()

// [
//   {
//     "_d": "title",
//     "_e": 0,
//     "_p": 0,
//     "textContent": "My title",
//     "props": {},
//     "tag": "title",
//   },
//   {
//     "_d": "meta:name:description",
//     "_e": 0,
//     "_p": 1,
//     "props": {
//       "content": "My description",
//       "name": "description",
//     },
//     "tag": "meta",
//   },
// ]

When resolving the tags, the following steps are taken:

Call hook entries:resolve

Resolve any reactive elements within the input

Call hook tags:normalise

Normalise the tags, makes sure the schema of the tag is correct:

  1. Assigns a dedupe key to the tag
  2. Handles deprecated options
  3. Assigns tag meta data (entry id, position id, etc)

Call hook tags:resolve

Process the hooks for default plugin logic:

  1. Deduping tags, including class and style merging
  2. Ordering tags
  3. Handling title template

Frameworks integrations will abstract these lifecycle functions away, but they are available for custom integrations.

@unhead/dom

By default, when entries are updated, the hook entries:updated is called, which will trigger a debounced DOM update.

The DOM rendered resolves the tags as above and starts patching

The DOM patching algorithm is non-aggressive and will preserve existing state as much as possible.

To support mismatch SSR / CSR content, Unhead assigns a data-h-key="${hash}" key to rendered attributes when a key is provided.

Side effects

When rendering elements or attributes, side effects are collected with the head entry to dispose of when needed.

When you dispose or update the entry, these side effects will be cleared.

myFirstEntry.dispose()
// next time the DOM render is called, the side effects will be cleared