-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Description
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
- Build and run with
@astrojs/node - Open the page — the first Counter works, the second does not
- Inspect the
/_server-islands/DeferredWrapperresponse 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
.instructionsarray 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.