TypeScript

Extending Unhead

Introduction

Unhead is designed with extensibility in mind, providing lower-level primitives that can be composed to create powerful functionality. This guide explores how to extend Unhead using hooks and plugins to meet your specific requirements.

Understanding the Architecture

Unhead uses a hooks-based architecture powered by unjs/hookable, allowing you to tap into different parts of the head tag management lifecycle. This enables you to create custom features without modifying the core library.

Hook Execution Sequence

Understanding the order in which hooks are executed is important for creating plugins that work well together. Here is the typical flow:

  1. Initialization: init
  2. Entry Processing:
  • entries:updated
  • entries:resolve
  • For each entry: entries:normalize
  1. Tag Processing:
  • For each tag: tag:normalise
  • tags:beforeResolve
  • tags:resolve
  • tags:afterResolve
  1. Client-side Rendering:
  • dom:beforeRender
  • For each tag: dom:renderTag
  • dom:rendered
  1. Server-side Rendering:
  • ssr:beforeRender
  • ssr:render
  • ssr:rendered
  1. Script Management:
  • When applicable: script:updated

Available Hooks

Unhead provides several hooks you can use to extend functionality:

import { createHead, useHead } from 'unhead'

const head = createHead({
  hooks: {
    'entries:resolve': (ctx) => {
      // Called when entries need to be resolved to tags
    },
    'tags:resolve': (ctx) => {
      // Called when tags are being resolved for rendering
    },
    'tag:normalise': (ctx) => {
      // Called when a tag is being normalized
    },
    'tag:generated': (ctx) => {
      // Called after a tag has been generated
    }
    // See full list in the API reference
  }
})

Accessing Head State

The recommended way to access the head state is through the resolveTags function:

import { injectHead, useHead } from 'unhead'

const head = injectHead()
const tags = await head.resolveTags()

// Now you can inspect or manipulate the tags
console.log(tags)

This gives you access to the fully processed tags that would be rendered to the DOM.

Creating Custom Composables

Unhead's composables like useHead() and useSeoMeta() are built on top of primitive APIs. You can create your own composables for specific use cases.

Example: Creating useTitle Composable

import { useHead } from 'unhead'

export function useTitle(title: string, options = {}) {
  return useHead({
    title,
  }, options)
}

Example: Creating useBodyClass Composable

import { useHead } from 'unhead'

export function useBodyClass(classes: string | string[]) {
  const classList = Array.isArray(classes) ? classes : [classes]

  return useHead({
    bodyAttrs: {
      class: classList.join(' ')
    }
  })
}

Building Plugins

For more complex extensions, you can create plugins that hook into multiple parts of Unhead's lifecycle.

Example: Custom Deduplication Plugin

import { defineHeadPlugin } from 'unhead'

export const customDedupePlugin = defineHeadPlugin({
  hooks: {
    'tags:resolve': (ctx) => {
      // Custom logic to deduplicate tags
      ctx.tags = deduplicateTagsWithCustomLogic(ctx.tags)
    }
  }
})

// Usage
const head = createHead({
  plugins: [
    customDedupePlugin()
  ]
})

Common Use Cases

Example: Tailwind Class Deduplication

This example shows how to deduplicate Tailwind CSS classes using tailwind-merge:

import { defineHeadPlugin } from 'unhead'
import { twMerge } from 'tailwind-merge'

export const tailwindMergePlugin = defineHeadPlugin({
  hooks: {
    'tags:resolve': (ctx) => {
      // Find body tags with class attributes
      ctx.tags.forEach((tag) => {
        if (tag.tag === 'bodyAttrs' && tag.props.class) {
          // Deduplicate classes with tailwind-merge
          tag.props.class = twMerge(tag.props.class)
        }
      })
    }
  }
})

Example: Custom MetaInfo Provider

Create a plugin that pulls meta information from a global store:

import { defineHeadPlugin } from 'unhead'

export const storeMetaPlugin = defineHeadPlugin({
  hooks: {
    'entries:resolve': (ctx) => {
      // Add entries from a store
      const storeMetaInfo = getMetaFromStore()
      ctx.entries.push(storeMetaInfo)
    }
  }
})

Best Practices

When extending Unhead:
  • Keep extensions focused on a single concern
  • Use typed hooks for better developer experience
  • Document your extensions for team usage
  • Consider performance implications in your hooks
  • Test extensions with a variety of input cases

API Reference

For a complete list of available hooks and their signatures, refer to the hooks definitions in the source code:

// From packages/unhead/src/types/hooks.ts
export interface HeadHooks {
  'init': () => void
  'entries:resolve': (ctx: CallbackParams<'entries:resolve'>) => void | Promise<void>
  'tags:resolve': (ctx: CallbackParams<'tags:resolve'>) => void | Promise<void>
  'tag:normalise': (ctx: TagAugmentation) => void | Promise<void>
  'tag:resolve': (ctx: CallbackParams<'tag:resolve'>) => void | Promise<void>
  'tag:validate': (ctx: TagAugmentation) => void | Promise<void>
  'tag:generated': (ctx: TagAugmentation) => void | Promise<void>
  // ...additional hooks
}
Did this page help you?