unhead@betaUnhead 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.
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.
Understanding the order in which hooks are executed is important for creating plugins that work well together. Here is the typical flow:
entries:updatedentries:resolveentries:normalizetag:normalisetags:beforeResolvetags:resolvetags:afterResolvedom:beforeRenderssr:beforeRenderssr:renderssr:renderedscript:updatedUnhead provides several hooks you can use to extend functionality:
import { createHead, useHead } from '@unhead/solid-js'
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
}
})
The recommended way to access the head state is through the resolveTags function:
import { injectHead, useHead } from '@unhead/solid-js'
const head = injectHead()
const tags = 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.
Unhead's composables like useHead() and useSeoMeta() are built on top of primitive APIs. You can create your own composables for specific use cases.
import { useHead } from '@unhead/solid-js'
export function useTitle(title: string, options = {}) {
return useHead({
title,
}, options)
}
import { useHead } from '@unhead/solid-js'
export function useBodyClass(classes: string | string[]) {
const classList = Array.isArray(classes) ? classes : [classes]
return useHead({
bodyAttrs: {
class: classList.join(' ')
}
})
}
For more complex extensions, you can create plugins that hook into multiple parts of Unhead's lifecycle.
import { defineHeadPlugin } from '@unhead/solid-js'
export const customDedupePlugin = defineHeadPlugin({
hooks: {
'tags:resolve': (ctx) => {
// Custom logic to deduplicate tags
ctx.tags = deduplicateTagsWithCustomLogic(ctx.tags)
}
}
})
// Usage
const head = createHead({
plugins: [
customDedupePlugin()
]
})
This example shows how to deduplicate Tailwind CSS classes using tailwind-merge:
import { defineHeadPlugin } from '@unhead/solid-js'
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)
}
})
}
}
})
Create a plugin that pulls meta information from a global store:
import { defineHeadPlugin } from '@unhead/solid-js'
export const storeMetaPlugin = defineHeadPlugin({
hooks: {
'entries:resolve': (ctx) => {
// Add entries from a store
const storeMetaInfo = getMetaFromStore()
ctx.entries.push(storeMetaInfo)
}
}
})
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 {
'entries:updated': (ctx: Unhead<any>) => HookResult
'entries:resolve': (ctx: EntryResolveCtx<any>) => SyncHookResult
'entries:normalize': (ctx: { tags: HeadTag[], entry: HeadEntry<any> }) => SyncHookResult
'tag:normalise': (ctx: { tag: HeadTag, entry: HeadEntry<any>, resolvedOptions: CreateClientHeadOptions }) => SyncHookResult
'tags:beforeResolve': (ctx: TagResolveContext) => SyncHookResult
'tags:resolve': (ctx: TagResolveContext) => SyncHookResult
'tags:afterResolve': (ctx: TagResolveContext) => SyncHookResult
'dom:beforeRender': (ctx: DomBeforeRenderCtx) => SyncHookResult
// ...additional hooks
}
Note: SyncHookResult is void (synchronous only), while HookResult is void | Promise<void>.