Skip to content

feat: access resolved head with onRendered#712

Merged
harlan-zw merged 4 commits into
mainfrom
worktree-feat+on-rendered-option
Apr 5, 2026
Merged

feat: access resolved head with onRendered#712
harlan-zw merged 4 commits into
mainfrom
worktree-feat+on-rendered-option

Conversation

@harlan-zw
Copy link
Copy Markdown
Collaborator

@harlan-zw harlan-zw commented Apr 4, 2026

Summary

  • Adds onRendered callback option to HeadEntryOptions, enabling users to synchronize external tools (e.g. analytics) with DOM head updates
  • Works across all frameworks (Vue, React, Svelte, Solid, Angular) with zero new API exports — piggybacks on existing useHead() / useSeoMeta() / useHeadSafe() options
  • Callback is client-only (ignored during SSR) and automatically cleaned up on entry disposal / component unmount
  • Alternative approach to feat(vue): add onHeadUpdated composable for DOM sync #685 — instead of a new onHeadUpdated() composable, extends existing options to avoid growing the API surface
useHead({ title: 'My Page' }, {
  onRendered({ renders }) {
    analytics.track('Page View', { title: document.title })
  }
})

Resolves #615
Supersedes #685

Changes

  • packages/unhead/src/types/head.ts — Added onRendered to HeadEntryOptions
  • packages/unhead/src/unhead.ts — Hook registration in push(), cleanup in dispose()
  • packages/unhead/test/unit/client/onRendered.test.ts — 7 core tests
  • packages/vue/test/unit/dom/onRendered.test.ts — 5 Vue integration tests
  • docs/head/7.api/composables/0.use-head.md — API docs and usage guide
  • 7 migration guides updated to reference onRendered

Test plan

  • Core tests: callback fires, receives context, fires on every render, stops after dispose, ignored during SSR, multiple entries, independent dispose
  • Vue tests: useHead integration, reactive updates, dispose cleanup, useSeoMeta compatibility
  • All existing tests pass (621 core + 124 Vue)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an onRendered option to useHead() to run client-only callbacks after DOM head updates; invoked on every render and cleaned up when the entry is disposed.
  • Documentation

    • Updated migration guides to replace prior dom:rendered guidance with onRendered usage.
    • Added API docs describing onRendered semantics, timing, payload, and lifecycle.
  • Tests

    • Added client and framework tests validating onRendered invocation, context payload, render-update behavior, and disposal handling.

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) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

Adds a client-only onRendered callback to HeadEntryOptions, wires it into the client head flow (registers/unregisters a dom:rendered hook), updates docs and types, and adds unit and integration tests verifying invocation and lifecycle behavior.

Changes

Cohort / File(s) Summary
Migration Guide Updates
docs/0.angular/head/guides/.../2.migration.md, docs/0.nuxt/head/guides/.../1.migration.md, docs/0.react/head/guides/.../2.migration.md, docs/0.solid-js/head/guides/.../2.migration.md, docs/0.svelte/head/guides/.../2.migration.md, docs/0.typescript/head/guides/.../1.migration.md, docs/0.vue/head/guides/.../1.migration.md
Replaced references to the removed dom:rendered hook with guidance to use the onRendered option on useHead() across framework migration docs.
API Documentation
docs/head/7.api/composables/0.use-head.md
Added HeadEntryOptions.onRendered docs, described client-only semantics, per-render { renders: DomRenderTagContext[] } context, cleanup, and applicability to useHead/useSeoMeta/useHeadSafe.
Type Definitions
packages/unhead/src/types/head.ts
Added `onRendered?: (ctx: { renders: DomRenderTagContext[] }) => void
Client Implementation
packages/unhead/src/client/createHead.ts, packages/unhead/src/unhead.ts
push() registers _options.onRendered as a dom:rendered hook (stores unhook) and removes onRendered from stored entry options; entry dispose() calls unhook to unregister.
Runtime Framework Wrappers
packages/solid-js/src/composables.ts, packages/vue/src/composables.ts
Wrap options.onRendered on the client to preserve framework reactive context/owner/scope when present (Solid: runWithOwner, Vue: scope.run).
Unit Tests (unhead)
packages/unhead/test/unit/client/onRendered.test.ts
New tests verifying client-side onRendered invocation after DOM updates, renders contents, repeated calls on updates, disposal stops calls, SSR ignores callback, and multi-entry isolation.
Integration Tests (Vue)
packages/vue/test/unit/dom/onRendered.test.ts
Vue CSR tests confirming onRendered fires after DOM render for useHead and useSeoMeta, receives correct render context, re-fires on reactive updates, and stops after disposal/unmount.

Sequence Diagram(s)

sequenceDiagram
  participant App as Client App
  participant CH as createHead (client)
  participant Core as unhead Core
  participant DOM as DOM renderer

  App->>CH: push(input, { onRendered })
  CH->>Core: core.push(input, optionsWithoutCallbacks)
  CH-->>CH: register dom:rendered hook (store unhook)
  Core->>DOM: apply DOM updates
  DOM->>CH: dom:rendered event with renders[]
  CH->>App: invoke onRendered({ renders })
  App->>CH: entry.dispose()
  CH->>CH: call unhook() (cleanup)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐰 I hop as DOM updates land,

onRendered clasped in my tiny hand,
Titles settle, callbacks sing,
Hooks fire true with every ping,
A joyful hop for each render.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description covers the summary, changes, and test plan but does not explicitly mark the type of change with checkboxes from the template.
Linked Issues check ✅ Passed The PR successfully implements the core requirement from #615: providing a client-side callback that fires after DOM head updates to synchronize external tools like analytics.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the onRendered callback feature across the codebase, documentation, and tests. No unrelated changes detected.
Title check ✅ Passed The title accurately describes the main feature added: a new onRendered callback option that allows accessing resolved head content after DOM updates.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-feat+on-rendered-option

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 4, 2026

Bundle Size Analysis

Bundle Size Gzipped
Client (Minimal) 10.5 kB → 10.5 kB 🔴 +0.1 kB 4.3 kB
Server (Minimal) 10.3 kB 4.2 kB
Vue Client (Minimal) 11.4 kB → 11.6 kB 🔴 +0.2 kB 4.7 kB → 4.8 kB 🔴 +0.1 kB
Vue Server (Minimal) 11.1 kB → 11.2 kB 🔴 +0.1 kB 4.6 kB → 4.6 kB 🔴 +0.1 kB

harlan-zw and others added 3 commits April 5, 2026 13:16
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@harlan-zw harlan-zw changed the title feat(unhead): add onRendered option to HeadEntryOptions feat: access resolved head with onRendered Apr 5, 2026
@harlan-zw harlan-zw merged commit 55166ee into main Apr 5, 2026
6 checks passed
@harlan-zw harlan-zw mentioned this pull request Apr 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Vue] Can't synchronise head title with analytics tools

1 participant