Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cute-times-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

support nested components in <svelte:head>
23 changes: 23 additions & 0 deletions packages/svelte/src/internal/client/dom/blocks/svelte-head.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,30 @@ export function head(hash, render_fn) {
}

try {
// Track nodes added to head before render
const head_children_before = Array.from(document.head.children);
const head_child_count_before = head_children_before.length;

block(() => render_fn(anchor), HEAD_EFFECT);

// After rendering, check if non-head elements were added and move them to body
Copy link
Contributor

Choose a reason for hiding this comment

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

I very doubt this logic makes sense. Aside from "Why do you mix head and body elements?", these elements became un-cleanable in case the component/head is wrapped in #if or something else dynamic. And probably breaks other blocks like #each.

const head_child_count_after = document.head.children.length;
if (head_child_count_after > head_children_before.length) {
// Elements were added to head, check if any are non-head elements
const new_children = Array.from(document.head.children).slice(head_child_count_before);
for (const child of new_children) {
// Move non-head-specific elements to body
if (child.nodeType === 1) {
// ELEMENT_NODE
const tag = child.tagName.toLowerCase();
// Only keep head-specific elements (script, meta, link, style, title, base, noscript)
// Move div, span, and other body elements to the body
if (!['script', 'meta', 'link', 'style', 'title', 'base', 'noscript'].includes(tag)) {
document.body.appendChild(child);
}
}
}
}
} finally {
if (was_hydrating) {
set_hydrating(true);
Expand Down
28 changes: 24 additions & 4 deletions packages/svelte/src/internal/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,30 @@ export function render(component, options = {}) {
* @returns {void}
*/
export function head(hash, renderer, fn) {
renderer.head((renderer) => {
renderer.push(`<!--${hash}-->`);
renderer.child(fn);
renderer.push(EMPTY_COMMENT);
renderer.head((parent_renderer) => {
parent_renderer.push(`<!--${hash}-->`);

// Create a capture renderer to collect all output from the function
const capture_renderer = new Renderer(parent_renderer.global, parent_renderer);
const result = fn(capture_renderer);

if (result instanceof Promise) {
return result.then(() => {
// Collect content and only push head content
const content = capture_renderer.collect_sync();
if (content.head) {
parent_renderer.push(content.head);
}
});
} else {
// Collect content and only push head content
const content = capture_renderer.collect_sync();
if (content.head) {
parent_renderer.push(content.head);
}
}

parent_renderer.push(EMPTY_COMMENT);
});
}

Expand Down
8 changes: 8 additions & 0 deletions packages/svelte/src/internal/server/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,14 @@ export class Renderer {
return this.#out.length;
}

/**
* Collect all content from this renderer (both head and body) synchronously.
* @returns {AccumulatedContent}
*/
collect_sync() {
return this.#collect_content();
}

/**
* Only available on the server and when compiling with the `server` option.
* Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<svelte:head>
<meta name="child-data" content="value" />
</svelte:head>

<p>Child body</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { test } from '../../assert';

export default test({
async test({ assert, target }) {
// Test 1: Verify body content appears in body, not head
const mainContent = target.querySelector('main');
assert.ok(mainContent, 'Main element should exist in body');
assert.equal(mainContent?.textContent, 'Main content');

// Test 2: Verify head contains meta tag (head-specific)
const metaTag = document.head.querySelector('meta[name="child-data"]');
assert.ok(metaTag, 'Meta tag should be in head');
assert.equal(metaTag?.getAttribute('content'), 'value');

// Test 3: Verify title is in head
const titleTag = document.head.querySelector('title');
assert.ok(titleTag, 'Title should be in head');
assert.equal(titleTag?.textContent, 'CSR Test');

// Test 4: Verify body elements are NOT in head
const pInHead = document.head.querySelector('p');
assert.equal(pInHead, null, 'Paragraph element should NOT be in head');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script>
import Child from './Child.svelte';
</script>

<svelte:head>
<title>CSR Test</title>
<Child />
</svelte:head>

<main>Main content</main>
233 changes: 0 additions & 233 deletions packages/svelte/tests/runtime-browser/test.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<svelte:head>
<meta name="child-meta" content="test" />
</svelte:head>

<div>Child body</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { test } from '../../test';

export default test({
mode: ['async', 'sync']
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!--[--><main>Main content</main><!--]-->
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!--22lmcx--><!--5uyrne--><meta name="child-meta" content="test"/><!----><div>Child body</div><!----><title>Parent Title</title>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<script>
import Child from './Child.svelte';
</script>

<svelte:head>
<title>Parent Title</title>
<Child />
</svelte:head>

<main>Main content</main>
Loading