Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 55166ee

Browse files
harlan-zwclaude
andauthored
feat: access resolved head with onRendered (#712)
* feat(unhead): add `onRendered` option to `HeadEntryOptions` Adds a client-side callback option to useHead/useSeoMeta/useHeadSafe that fires after DOM updates are applied. This enables synchronizing external tools (e.g. analytics) with the current document head state. The callback is automatically cleaned up when the entry is disposed (including on component unmount). Ignored during SSR. Resolves #615 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix: register onRendered hook in client head, not core The client createHead wraps core push() with its own push(), and hooks are only available on the client wrapper. Moved hook registration to the client head's push() where hooks are accessible. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(unhead): register onRendered hook before triggering entries:updated The onRendered hook was registered after entries:updated was called, but entries:updated synchronously triggers the renderer which fires dom:rendered. This meant the callback was never invoked on the initial render. Also fixes multi-entry test expectations to account for each push() triggering an immediate render, and adds a Vue component unmount test to verify onRendered cleanup on component lifecycle. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> * fix(vue,solid): preserve component scope for onRendered callbacks Wrap onRendered callbacks to run within the registering component's scope context. This ensures reactive tracking works correctly and provides an extra safety layer — disposed scopes silently no-op. - Vue: capture getCurrentScope(), run callback via scope.run() - Solid: capture getOwner(), run callback via runWithOwner() - React/Svelte/Angular: no scope concept — closures + disposal cleanup already handle this correctly Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 4c785c0 commit 55166ee

15 files changed

Lines changed: 310 additions & 9 deletions

File tree

docs/0.angular/head/guides/0.get-started/2.migration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ if (isPlatformBrowser(this.platformId)) {
9999
The following hooks have been removed:
100100
- `init` - No longer needed
101101
- `dom:renderTag` - DOM rendering is now synchronous
102-
- `dom:rendered` - Use code after `renderDOMHead()` instead
102+
- `dom:rendered` - Use the `onRendered` option on `useHead()` instead
103103

104104
The `dom:beforeRender` hook is now synchronous and `renderDOMHead` no longer returns a Promise:
105105

docs/0.nuxt/head/guides/0.get-started/1.migration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ if (import.meta.server) {
9696
If you're using Unhead hooks directly:
9797
- `init` hook removed
9898
- `dom:renderTag` hook removed
99-
- `dom:rendered` hook removed
99+
- `dom:rendered` hook removed - use the `onRendered` option on `useHead()` instead
100100
- `dom:beforeRender` is now synchronous
101101
- `renderDOMHead` is now synchronous
102102

docs/0.react/head/guides/0.get-started/2.migration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ if (typeof window !== 'undefined') {
9797
The following hooks have been removed:
9898
- `init` - No longer needed
9999
- `dom:renderTag` - DOM rendering is now synchronous
100-
- `dom:rendered` - Use code after `renderDOMHead()` instead
100+
- `dom:rendered` - Use the `onRendered` option on `useHead()` instead
101101

102102
The `dom:beforeRender` hook is now synchronous and `renderDOMHead` no longer returns a Promise:
103103

docs/0.solid-js/head/guides/0.get-started/2.migration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ if (!isServer) {
9999
The following hooks have been removed:
100100
- `init` - No longer needed
101101
- `dom:renderTag` - DOM rendering is now synchronous
102-
- `dom:rendered` - Use code after `renderDOMHead()` instead
102+
- `dom:rendered` - Use the `onRendered` option on `useHead()` instead
103103

104104
The `dom:beforeRender` hook is now synchronous and `renderDOMHead` no longer returns a Promise:
105105

docs/0.svelte/head/guides/0.get-started/2.migration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ if (browser) {
9999
The following hooks have been removed:
100100
- `init` - No longer needed
101101
- `dom:renderTag` - DOM rendering is now synchronous
102-
- `dom:rendered` - Use code after `renderDOMHead()` instead
102+
- `dom:rendered` - Use the `onRendered` option on `useHead()` instead
103103

104104
The `dom:beforeRender` hook is now synchronous and `renderDOMHead` no longer returns a Promise:
105105

docs/0.typescript/head/guides/0.get-started/1.migration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ head.push({
133133
The following hooks have been removed:
134134
- `init` - No longer needed
135135
- `dom:renderTag` - DOM rendering is now synchronous
136-
- `dom:rendered` - Use code after `renderDOMHead()` instead
136+
- `dom:rendered` - Use the `onRendered` option on `useHead()` instead
137137

138138
The `dom:beforeRender` hook is now synchronous and `renderDOMHead` no longer returns a Promise:
139139

docs/0.vue/head/guides/0.get-started/1.migration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ useHead({
149149
The following hooks have been removed:
150150
- `init` - No longer needed
151151
- `dom:renderTag` - DOM rendering is now synchronous
152-
- `dom:rendered` - Use code after `renderDOMHead()` instead
152+
- `dom:rendered` - Use the `onRendered` option on `useHead()` instead
153153

154154
The `dom:beforeRender` hook is now synchronous and `renderDOMHead` no longer returns a Promise:
155155

docs/head/7.api/composables/0.use-head.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ export interface HeadEntryOptions {
148148
// Where to position tags in the document
149149
tagPosition?: 'head' | 'bodyClose' | 'bodyOpen'
150150
151+
// Callback fired after DOM updates are applied (client-only, ignored during SSR)
152+
onRendered?: (ctx: { renders: DomRenderTagContext[] }) => void | Promise<void>
153+
151154
// Custom head instance
152155
head?: Unhead
153156
}
@@ -177,6 +180,32 @@ useHead({
177180
The `tagPriority`{lang="ts"} option is particularly useful for controlling render order when you have multiple head entries that might contain similar tags.
178181
::
179182

183+
## Synchronizing with DOM Updates
184+
185+
The `onRendered` option lets you run a callback after unhead has finished applying DOM updates.
186+
This is useful for synchronising external tools like analytics with the current document head (e.g. reading `document.title`
187+
after it has been set).
188+
189+
```ts
190+
import { useHead } from '@unhead/dynamic-import'
191+
192+
useHead({
193+
title: 'My Page',
194+
}, {
195+
onRendered({ renders }) {
196+
// document.title is guaranteed to be up-to-date here
197+
analytics.track('Page View', { title: document.title })
198+
}
199+
})
200+
```
201+
202+
- The callback fires on **every** DOM render, not just the first.
203+
- It is **ignored during SSR**only runs on the client.
204+
- The callback is automatically cleaned up when the entry is disposed (including on component unmount in frameworks).
205+
- The `renders` array contains the render context for each tag that was processed.
206+
207+
Works with all composables that accept `HeadEntryOptions``useHead()`, `useSeoMeta()`, and `useHeadSafe()`.
208+
180209
## Reactivity
181210

182211
::VueOnly

packages/solid-js/src/composables.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {
99
UseScriptReturn,
1010
UseSeoMetaInput,
1111
} from 'unhead/types'
12-
import { createEffect, createSignal, onCleanup, onMount, useContext } from 'solid-js'
12+
import { createEffect, createSignal, getOwner, onCleanup, onMount, runWithOwner, useContext } from 'solid-js'
1313
import { isServer } from 'solid-js/web'
1414
import { useHead as baseHead, useHeadSafe as baseHeadSafe, useSeoMeta as baseSeoMeta, useScript as baseUseScript } from 'unhead'
1515
import { UnheadContext } from './context'
@@ -25,6 +25,14 @@ export function useUnhead(): Unhead {
2525

2626
function withSideEffects<T extends ActiveHeadEntry<any>>(input: any, options: any, fn: any): T {
2727
const unhead = options.head || useUnhead()
28+
// Wrap onRendered to preserve the Solid reactive owner context
29+
if (options.onRendered && !isServer) {
30+
const owner = getOwner()
31+
if (owner) {
32+
const _onRendered = options.onRendered
33+
options = { ...options, onRendered: (ctx: any) => runWithOwner(owner, () => _onRendered(ctx)) }
34+
}
35+
}
2836
const entry = fn(unhead, input, options) as T
2937
// On client, set up reactive updates and cleanup
3038
if (!isServer) {

packages/unhead/src/client/createHead.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export function createHead<T = ResolvableHead>(options: CreateClientHeadOptions
3434
hooks.callHook('entries:updated', head)
3535
},
3636
push(input: T, _options?: HeadEntryOptions) {
37+
const onRendered = _options?.onRendered
38+
const unhook = onRendered
39+
? hooks.hook('dom:rendered', onRendered as any)
40+
: undefined
3741
const active = core.push(input, _options)
3842
core.entries.get(active._i)!._o = input
3943
dirty = true
@@ -46,6 +50,7 @@ export function createHead<T = ResolvableHead>(options: CreateClientHeadOptions
4650
hooks.callHook('entries:updated', head)
4751
},
4852
dispose() {
53+
unhook?.()
4954
if (core.entries.has(active._i)) {
5055
active.dispose()
5156
head.invalidate()

0 commit comments

Comments
 (0)