Skip to content

fix(vue): restore Style component inside Head and children prop#686

Merged
harlan-zw merged 2 commits into
mainfrom
fix/517-vue-style-head-component
Apr 5, 2026
Merged

fix(vue): restore Style component inside Head and children prop#686
harlan-zw merged 2 commits into
mainfrom
fix/517-vue-style-head-component

Conversation

@harlan-zw

@harlan-zw harlan-zw commented Mar 10, 2026

Copy link
Copy Markdown
Collaborator

🔗 Linked issue

Resolves #517

❓ Type of change

  • 📖 Documentation
  • 🐞 Bug fix
  • 👌 Enhancement
  • ✨ New feature
  • 🧹 Chore
  • ⚠️ Breaking change

📚 Description

addVNodeToHeadObj had two bugs introduced in v2: it read node.children[0]['children'] which was off by one indirection for text VNodes, and stored extracted content as props.children which isn't a recognized key in unhead v3. Fixed by adding extractTextContent() helper that handles all VNode child shapes, and mapping the content to textContent (for style/noscript/title) or innerHTML (for script). Also restores backward-compat children prop support.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Enhanced Head component handling of style, script, and title element content
    • Improved text content normalization when content is passed as children to head elements
  • Tests

    • Added comprehensive unit tests for Head component styling and script handling

@coderabbitai

coderabbitai Bot commented Mar 10, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

The changes fix handling of VNode children text content in the Vue Head component by introducing a helper function to recursively extract plain text, normalizing props to use tag-specific keys (textContent for most tags, innerHTML for scripts), and updating title resolution logic to reference the normalized props.

Changes

Cohort / File(s) Summary
Head Component Logic
packages/vue/src/components.ts
Adds extractTextContent() helper to recursively derive plain strings from VNode children. Refactors addVNodeToHeadObj to clone props via spread, select innerKey based on tag type (script→innerHTML, else→textContent), normalize legacy props.children to props[innerKey], and update title resolution to use normalized prop keys.
Test Coverage
packages/vue/test/unit/dom/headComponents.test.ts
New comprehensive test suite for Head component's style/script tag handling, verifying text content extraction from children props, direct vnode children, reactive arrays, and end-to-end DOM rendering with jsdom.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 The children once lost in the VNode tree,
Now bloom where the Head can clearly see!
Script and style, each with their own key,
Text extraction magic sets properties free! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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
Title check ✅ Passed The title clearly and concisely describes the main change: fixing the Vue Style component inside Head and restoring children prop support.
Description check ✅ Passed The description includes all required sections: linked issue (#517), type of change (bug fix), and detailed technical explanation of the bugs and fixes.
Linked Issues check ✅ Passed The PR addresses the core coding requirements from issue #517: restoring Style component support inside Head by fixing VNode child extraction and mapping content to the correct property key.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing issue #517: the helper function, prop cloning, innerKey selection, and text extraction logic all serve the stated objective of restoring Style/children prop support.

✏️ 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 fix/517-vue-style-head-component

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

github-actions Bot commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Bundle Size Gzipped
Client (Minimal) 10.6 kB 4.3 kB
Server (Minimal) 10.3 kB 4.2 kB
Vue Client (Minimal) 11.6 kB 4.8 kB
Vue Server (Minimal) 11.3 kB 4.6 kB

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/vue/test/unit/dom/headComponents.test.ts (1)

79-103: Exercise a multi-node children case here.

[css.value] only covers the one-item happy path. It won't fail when extractTextContent() ignores sibling text nodes, which is the shape Vue often produces for adjacent interpolations or fragments. Add at least one case with two text entries or a fragment-wrapped text child.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue/test/unit/dom/headComponents.test.ts` around lines 79 - 103,
Update the test "Style with dynamic slot content (array children)" to exercise a
multi-node children shape (two adjacent text nodes or a fragment-wrapped text
child) instead of only [css.value]; specifically, when building the Head slot in
the App component (the render returning h(Head,...)), change the style children
to include two text entries (e.g., [css.value, ' /* extra */'] or wrap with a
Fragment that yields two text nodes) so resolveTags/ extractTextContent must
concatenate siblings and the assertion on styleTag?.textContent still verifies
the combined string.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/vue/src/components.ts`:
- Around line 11-18: The current extractTextContent logic only reads the first
array child and truncates multi-part text; update the Array.isArray(children)
branch in extractTextContent to iterate over all entries, map each entry through
the existing extraction logic (treat string entries as text, for objects with a
'children' property recurse via extractTextContent on VNode['children']),
collect the results and return the joined string (e.g., concatenated with ''),
ensuring other branches (string and object-with-children) remain unchanged.

In `@packages/vue/test/unit/dom/headComponents.test.ts`:
- Around line 12-25: The unused helper mountWithHead is causing CI failures and
uses CommonJS require; either remove the entire mountWithHead function or
convert it to a module-style helper and use it in tests: replace const {
createApp } = require('vue') with an ES import (import { createApp } from
'vue'), keep references to createHead and App as shown, ensure tests call
mountWithHead(renderFn) so it’s used; otherwise delete the mountWithHead
definition and any related unused symbols (createHead, App) from the file.

---

Nitpick comments:
In `@packages/vue/test/unit/dom/headComponents.test.ts`:
- Around line 79-103: Update the test "Style with dynamic slot content (array
children)" to exercise a multi-node children shape (two adjacent text nodes or a
fragment-wrapped text child) instead of only [css.value]; specifically, when
building the Head slot in the App component (the render returning h(Head,...)),
change the style children to include two text entries (e.g., [css.value, ' /*
extra */'] or wrap with a Fragment that yields two text nodes) so resolveTags/
extractTextContent must concatenate siblings and the assertion on
styleTag?.textContent still verifies the combined string.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ae5694b6-05a4-4f55-ad06-50e52a45e60f

📥 Commits

Reviewing files that changed from the base of the PR and between beeb5f1 and 44e8b5b.

📒 Files selected for processing (2)
  • packages/vue/src/components.ts
  • packages/vue/test/unit/dom/headComponents.test.ts

Comment thread packages/vue/src/components.ts
Comment thread packages/vue/test/unit/dom/headComponents.test.ts Outdated
@harlan-zw harlan-zw force-pushed the fix/517-vue-style-head-component branch from 44e8b5b to ab501c3 Compare April 5, 2026 16:33

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/vue/test/unit/dom/headComponents.test.ts`:
- Line 10: The file imports mount but never uses it, causing an ESLint error;
remove the unused import by deleting "mount" from the import statement (the
import line that currently reads "import { mount } from '../../util'") or
replace it with a used symbol from '../../util' if intended—ensure no other
references to mount exist in headComponents.test.ts and run lint/tests to
confirm.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8bfd7612-77fd-4292-8c85-ce53bbeceed2

📥 Commits

Reviewing files that changed from the base of the PR and between 44e8b5b and ab501c3.

📒 Files selected for processing (2)
  • packages/vue/src/components.ts
  • packages/vue/test/unit/dom/headComponents.test.ts

Comment thread packages/vue/test/unit/dom/headComponents.test.ts Outdated
@harlan-zw harlan-zw force-pushed the fix/517-vue-style-head-component branch from ab501c3 to 70ffaee Compare April 5, 2026 16:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/vue/test/unit/dom/headComponents.test.ts (1)

106-106: Drop unnecessary await for synchronous renderDOMHead().

renderDOMHead() is synchronous here, so awaiting it adds noise and hides the intended API usage.

Suggested cleanup
-    await renderDOMHead(head, { document: dom.window.document })
+    renderDOMHead(head, { document: dom.window.document })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue/test/unit/dom/headComponents.test.ts` at line 106, renderDOMHead
is synchronous in this test, so remove the unnecessary "await" before the
renderDOMHead(head, { document: dom.window.document }) call; replace the awaited
invocation with a plain call to renderDOMHead(...) and, if the test function is
only async because of that await, change the test to a non-async function
(remove the async keyword) to reflect the synchronous API.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/0.react/head/guides/1.core-concepts/2.reactivity.md`:
- Line 276: The ordered list item "Cleanup Happens Automatically: Unhead handles
cleanup when components unmount through React's effect cleanup system." is
incorrectly numbered as "1." after items 1–3; update its markdown list marker to
"4." (or the next sequential number used in the preceding list) so the
ordered-list numbering is consistent in the source.

In `@docs/head/1.guides/releases/v3.md`:
- Line 8: Replace the unhyphenated compound adjective "side-effect free" with
the hyphenated form "side-effect-free" in the sentence that describes the new
rendering engine; update the phrase in the paragraph containing "To fix this
properly, we had to make rendering synchronous, pluggable, and side-effect free"
so it reads "side-effect-free" for correct compound adjective usage and improved
readability.

---

Nitpick comments:
In `@packages/vue/test/unit/dom/headComponents.test.ts`:
- Line 106: renderDOMHead is synchronous in this test, so remove the unnecessary
"await" before the renderDOMHead(head, { document: dom.window.document }) call;
replace the awaited invocation with a plain call to renderDOMHead(...) and, if
the test function is only async because of that await, change the test to a
non-async function (remove the async keyword) to reflect the synchronous API.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3959bee7-b6e2-43b7-9947-b4341d726cce

📥 Commits

Reviewing files that changed from the base of the PR and between ab501c3 and 8bd589d.

📒 Files selected for processing (47)
  • docs/0.angular/head/guides/0.get-started/1.installation.md
  • docs/0.angular/head/guides/0.get-started/2.migration.md
  • docs/0.angular/head/guides/1.core-concepts/0.reactivity.md
  • docs/0.angular/schema-org/guides/get-started/0.installation.md
  • docs/0.nuxt/head/guides/0.get-started/1.migration.md
  • docs/0.react/head/guides/0.get-started/1.installation.md
  • docs/0.react/head/guides/0.get-started/2.migration.md
  • docs/0.react/head/guides/0.get-started/migrate-from-react-helmet.md
  • docs/0.react/head/guides/1.core-concepts/2.reactivity.md
  • docs/0.react/schema-org/guides/get-started/0.installation.md
  • docs/0.solid-js/head/guides/0.get-started/1.installation.md
  • docs/0.solid-js/head/guides/0.get-started/2.migration.md
  • docs/0.solid-js/head/guides/1.core-concepts/0.reactivity.md
  • docs/0.solid-js/schema-org/guides/get-started/0.installation.md
  • docs/0.svelte/head/guides/0.get-started/1.installation.md
  • docs/0.svelte/head/guides/0.get-started/2.migration.md
  • docs/0.svelte/schema-org/guides/get-started/0.installation.md
  • docs/0.typescript/head/guides/0.get-started/1.installation.md
  • docs/0.typescript/head/guides/0.get-started/1.migration.md
  • docs/0.typescript/schema-org/guides/get-started/0.installation.md
  • docs/0.vue/head/guides/0.get-started/1.installation.md
  • docs/0.vue/head/guides/0.get-started/1.migration.md
  • docs/0.vue/schema-org/guides/0.get-started/0.installation.md
  • docs/head/1.guides/1.core-concepts/1.titles.md
  • docs/head/1.guides/1.core-concepts/2.positions.md
  • docs/head/1.guides/1.core-concepts/3.class-attr.md
  • docs/head/1.guides/1.core-concepts/4.inner-content.md
  • docs/head/1.guides/1.core-concepts/6.handling-duplicates.md
  • docs/head/1.guides/1.core-concepts/8.dom-event-handling.md
  • docs/head/1.guides/1.core-concepts/9.loading-scripts.md
  • docs/head/1.guides/2.advanced/11.extending-unhead.md
  • docs/head/1.guides/2.advanced/7.client-only-tags.md
  • docs/head/1.guides/plugins/6.template-params.md
  • docs/head/1.guides/plugins/alias-sorting.md
  • docs/head/1.guides/plugins/canonical.md
  • docs/head/1.guides/plugins/infer-seo-meta-tags.md
  • docs/head/1.guides/plugins/validate.md
  • docs/head/1.guides/releases/v3.md
  • docs/head/7.api/composables/0.use-head.md
  • docs/head/7.api/composables/4.use-script.md
  • docs/schema-org/2.guides/0.get-started/0.overview.md
  • docs/schema-org/2.guides/4.recipes/0.custom-nodes.md
  • docs/schema-org/2.guides/4.recipes/blog.md
  • docs/schema-org/2.guides/4.recipes/faq.md
  • docs/schema-org/5.api/0.composables/0.use-schema-org.md
  • packages/vue/src/components.ts
  • packages/vue/test/unit/dom/headComponents.test.ts
✅ Files skipped from review due to trivial changes (44)
  • docs/0.svelte/schema-org/guides/get-started/0.installation.md
  • docs/0.solid-js/head/guides/0.get-started/1.installation.md
  • docs/0.solid-js/schema-org/guides/get-started/0.installation.md
  • docs/0.react/schema-org/guides/get-started/0.installation.md
  • docs/0.angular/schema-org/guides/get-started/0.installation.md
  • docs/0.vue/schema-org/guides/0.get-started/0.installation.md
  • docs/head/1.guides/1.core-concepts/9.loading-scripts.md
  • docs/0.react/head/guides/0.get-started/2.migration.md
  • docs/0.angular/head/guides/0.get-started/2.migration.md
  • docs/0.typescript/schema-org/guides/get-started/0.installation.md
  • docs/head/1.guides/1.core-concepts/8.dom-event-handling.md
  • docs/0.typescript/head/guides/0.get-started/1.installation.md
  • docs/0.vue/head/guides/0.get-started/1.installation.md
  • docs/0.svelte/head/guides/0.get-started/1.installation.md
  • docs/head/1.guides/1.core-concepts/3.class-attr.md
  • docs/head/7.api/composables/4.use-script.md
  • docs/0.solid-js/head/guides/0.get-started/2.migration.md
  • docs/0.react/head/guides/0.get-started/1.installation.md
  • docs/head/1.guides/1.core-concepts/4.inner-content.md
  • docs/0.angular/head/guides/0.get-started/1.installation.md
  • docs/head/1.guides/1.core-concepts/6.handling-duplicates.md
  • docs/schema-org/2.guides/0.get-started/0.overview.md
  • docs/head/1.guides/2.advanced/7.client-only-tags.md
  • docs/0.react/head/guides/0.get-started/migrate-from-react-helmet.md
  • docs/head/1.guides/plugins/6.template-params.md
  • docs/0.angular/head/guides/1.core-concepts/0.reactivity.md
  • docs/schema-org/2.guides/4.recipes/0.custom-nodes.md
  • docs/0.nuxt/head/guides/0.get-started/1.migration.md
  • docs/schema-org/2.guides/4.recipes/faq.md
  • docs/head/1.guides/plugins/alias-sorting.md
  • docs/0.vue/head/guides/0.get-started/1.migration.md
  • docs/schema-org/2.guides/4.recipes/blog.md
  • docs/head/1.guides/plugins/canonical.md
  • docs/0.svelte/head/guides/0.get-started/2.migration.md
  • docs/head/1.guides/1.core-concepts/1.titles.md
  • docs/head/7.api/composables/0.use-head.md
  • docs/schema-org/5.api/0.composables/0.use-schema-org.md
  • docs/head/1.guides/plugins/validate.md
  • docs/0.solid-js/head/guides/1.core-concepts/0.reactivity.md
  • docs/head/1.guides/2.advanced/11.extending-unhead.md
  • docs/head/1.guides/plugins/infer-seo-meta-tags.md
  • docs/0.typescript/head/guides/0.get-started/1.migration.md
  • packages/vue/src/components.ts
  • docs/head/1.guides/1.core-concepts/2.positions.md

```

4. **Cleanup Happens Automatically**: Unhead handles cleanup when components unmount through React's effect cleanup system.
1. **Cleanup Happens Automatically**: Unhead handles cleanup when components unmount through React's effect cleanup system.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep ordered-list numbering consistent in source markdown.

This item is written as 1. after items 1-3, which makes the source inconsistent and can render unexpectedly depending on parser/settings.

Proposed fix
-1. **Cleanup Happens Automatically**: Unhead handles cleanup when components unmount through React's effect cleanup system.
+4. **Cleanup Happens Automatically**: Unhead handles cleanup when components unmount through React's effect cleanup system.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
1. **Cleanup Happens Automatically**: Unhead handles cleanup when components unmount through React's effect cleanup system.
4. **Cleanup Happens Automatically**: Unhead handles cleanup when components unmount through React's effect cleanup system.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/0.react/head/guides/1.core-concepts/2.reactivity.md` at line 276, The
ordered list item "Cleanup Happens Automatically: Unhead handles cleanup when
components unmount through React's effect cleanup system." is incorrectly
numbered as "1." after items 1–3; update its markdown list marker to "4." (or
the next sequential number used in the preceding list) so the ordered-list
numbering is consistent in the source.

Comment thread docs/head/1.guides/releases/v3.md Outdated
title: "v3"
---

Unhead v3 rebuilds the rendering engine from the ground up. The motivation: **streaming SSR**. Frameworks like Nuxt, SolidStart, and SvelteKit stream HTML to the browser as data loads, but head tags were still stuck in a request/response model, resolved once and never updated. To fix this properly, we had to make rendering synchronous, pluggable, and side-effect free. The result is a faster, smaller, and more capable head manager.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hyphenate the compound adjective for readability.

Use side-effect-free instead of side-effect free in this sentence.

🧰 Tools
🪛 LanguageTool

[grammar] ~8-~8: Use a hyphen to join words.
Context: ... synchronous, pluggable, and side-effect free. The result is a faster, smaller, a...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/head/1.guides/releases/v3.md` at line 8, Replace the unhyphenated
compound adjective "side-effect free" with the hyphenated form
"side-effect-free" in the sentence that describes the new rendering engine;
update the phrase in the paragraph containing "To fix this properly, we had to
make rendering synchronous, pluggable, and side-effect free" so it reads
"side-effect-free" for correct compound adjective usage and improved
readability.

@harlan-zw harlan-zw force-pushed the fix/517-vue-style-head-component branch from 8bd589d to 954e680 Compare April 5, 2026 17:02
@harlan-zw harlan-zw merged commit f88c846 into main Apr 5, 2026
5 of 6 checks passed
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.

v2 breaks vue Style component inside Head component in Nuxt v3.16

1 participant