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:
- Initialization:
init
- Entry Processing:
entries:updated
entries:resolve
- For each entry:
entries:normalize
- Tag Processing:
- For each tag:
tag:normalise
tags:beforeResolve
tags:resolve
tags:afterResolve
- Client-side Rendering:
dom:beforeRender
- For each tag:
dom:renderTag
dom:rendered
- Server-side Rendering:
ssr:beforeRender
ssr:render
ssr:rendered
- 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
- 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
}