Skip to content

<script> tags silently dropped from components passed as slots to server:defer components #15622

@jwoyo

Description

@jwoyo

Astro Info

Astro                    v5.17.3
Vite                     v6.4.1
Node                     v25.2.1
System                   macOS (arm64)
Package Manager          pnpm
Output                   server
Adapter                  @astrojs/node (v9.5.4)
Integrations             none

Describe the Bug

When a component containing a <script> tag is passed as a slot to a server:defer component, the script is silently stripped from the server island response. The component's HTML renders correctly, but it has no interactivity.

There are no warnings or errors — the script simply disappears.

Reproduction

Minimal repo: https://stackblitz.com/~/github.com/jwoyo/astro-server-island-bug

Counter.astro — a simple component with a <script>:

<div class="counter">
  <p>Count: <span data-count>0</span></p>
  <button data-increment>Increment</button>
</div>

<script>
  document.querySelectorAll("[data-increment]").forEach((btn) => {
    btn.addEventListener("click", () => {
      const countEl = btn.previousElementSibling?.querySelector("[data-count]");
      if (countEl) {
        countEl.textContent = String(Number(countEl.textContent) + 1);
      }
    });
  });
</script>

DeferredWrapper.astro — a server:defer component:

<div>
  <slot name="content" />
</div>

index.astro:

---
import Counter from "../components/Counter.astro";
import DeferredWrapper from "../components/DeferredWrapper.astro";
---

<!-- ✅ Works: script is included in the page -->
<Counter />

<!-- 🐛 Broken: script is silently dropped from the server island response -->
<DeferredWrapper server:defer>
  <Counter slot="content" />
  <div slot="fallback">Loading...</div>
</DeferredWrapper>

Steps

  1. Build and run with @astrojs/node
  2. Open the page — the first Counter works, the second does not
  3. Inspect the /_server-islands/DeferredWrapper response in DevTools → Network

The response contains the Counter's HTML but no <script> tag:

<div>
  <div class="counter">
    <p>Count: <span data-count>0</span></p>
    <button data-increment>Increment</button>
  </div>
</div>

Expected Behavior

The server island response should include the <script> tags from components rendered within it. The second Counter should be interactive.

Root Cause (unverified)

In server-islands.js, ServerIslandComponent.getIslandContent() serializes slot content like this:

const content = await renderSlotToString(this.result, this.slots[name]);
renderedSlots[name] = content.toString();

renderSlotToString returns a SlotString which holds two things:

  • The HTML string (via .toString())
  • An .instructions array containing render instructions (scripts, directives, etc.)

Calling .toString() discards the .instructions, so any <script> associated with the slot content is lost before the slot is encrypted and sent to the island endpoint.

Workaround

Using <script is:inline> bypasses the render instruction system — the script is emitted as raw HTML, which survives .toString() serialization and is included in the island response.

What's the expected result?

The server island response should include the <script> tags from components rendered within it. The second Counter should be interactive.

Link to Minimal Reproducible Example

https://stackblitz.com/~/github.com/jwoyo/astro-server-island-bug

Participation

  • I am willing to submit a pull request for this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    - P4: importantViolate documented behavior or significantly impacts performance (priority)pkg: astroRelated to the core `astro` package (scope)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions