--- title: Extending Unhead description: Learn how to extend Unhead with hooks and plugins to create custom functionality navigation.title: 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](https://github.com/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` 3. **Tag Processing**: - For each tag: `tag:normalise` - `tags:beforeResolve` - `tags:resolve` - `tags:afterResolve` 4. **Client-side Rendering**: - `dom:beforeRender` - For each tag: `dom:renderTag` - `dom:rendered` 5. **Server-side Rendering**: - `ssr:beforeRender` - `ssr:render` - `ssr:rendered` 6. **Script Management**: - When applicable: `script:updated` ### Available Hooks Unhead provides several hooks you can use to extend functionality: ```ts import { createHead, useHead } from '@unhead/dynamic-import' 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: ```ts import { injectHead, useHead } from '@unhead/dynamic-import' 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 ```ts import { useHead } from '@unhead/dynamic-import' export function useTitle(title: string, options = {}) { return useHead({ title, }, options) } ``` ### Example: Creating useBodyClass Composable ```ts import { useHead } from '@unhead/dynamic-import' 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 ```ts import { defineHeadPlugin } from '@unhead/dynamic-import' 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`: ```ts import { defineHeadPlugin } from '@unhead/dynamic-import' 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: ```ts import { defineHeadPlugin } from '@unhead/dynamic-import' export const storeMetaPlugin = defineHeadPlugin({ hooks: { 'entries:resolve': (ctx) => { // Add entries from a store const storeMetaInfo = getMetaFromStore() ctx.entries.push(storeMetaInfo) } } }) ``` ## Best Practices ::tip 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: ```ts // 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 } ```