diff --git a/packages/astro/e2e/fixtures/server-islands/astro.config.mjs b/packages/astro/e2e/fixtures/server-islands/astro.config.mjs
new file mode 100644
index 000000000000..34e1ec5c6811
--- /dev/null
+++ b/packages/astro/e2e/fixtures/server-islands/astro.config.mjs
@@ -0,0 +1,11 @@
+import mdx from '@astrojs/mdx';
+import react from '@astrojs/react';
+import { defineConfig } from 'astro/config';
+import nodejs from '@astrojs/node';
+
+// https://astro.build/config
+export default defineConfig({
+ output: 'hybrid',
+ adapter: nodejs({ mode: 'standalone' }),
+ integrations: [react(), mdx()],
+});
diff --git a/packages/astro/e2e/fixtures/server-islands/package.json b/packages/astro/e2e/fixtures/server-islands/package.json
new file mode 100644
index 000000000000..9958ee287857
--- /dev/null
+++ b/packages/astro/e2e/fixtures/server-islands/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "@e2e/server-islands",
+ "version": "0.0.0",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev"
+ },
+ "dependencies": {
+ "@astrojs/react": "workspace:*",
+ "astro": "workspace:*",
+ "@astrojs/mdx": "workspace:*",
+ "@astrojs/node": "workspace:*",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ }
+}
diff --git a/packages/astro/e2e/fixtures/server-islands/src/components/Island.astro b/packages/astro/e2e/fixtures/server-islands/src/components/Island.astro
new file mode 100644
index 000000000000..a6d5fa3dc7f0
--- /dev/null
+++ b/packages/astro/e2e/fixtures/server-islands/src/components/Island.astro
@@ -0,0 +1,3 @@
+---
+---
+
I am an island
diff --git a/packages/astro/e2e/fixtures/server-islands/src/pages/index.astro b/packages/astro/e2e/fixtures/server-islands/src/pages/index.astro
new file mode 100644
index 000000000000..a620e7169cb2
--- /dev/null
+++ b/packages/astro/e2e/fixtures/server-islands/src/pages/index.astro
@@ -0,0 +1,12 @@
+---
+import Island from '../components/Island.astro';
+---
+
+
+
+
+
+
+
+
+
diff --git a/packages/astro/e2e/fixtures/server-islands/src/pages/mdx.mdx b/packages/astro/e2e/fixtures/server-islands/src/pages/mdx.mdx
new file mode 100644
index 000000000000..1a0a0ac6f3f9
--- /dev/null
+++ b/packages/astro/e2e/fixtures/server-islands/src/pages/mdx.mdx
@@ -0,0 +1,3 @@
+import Island from '../components/Island.astro';
+
+
diff --git a/packages/astro/e2e/server-islands.test.js b/packages/astro/e2e/server-islands.test.js
new file mode 100644
index 000000000000..0255b9f0563b
--- /dev/null
+++ b/packages/astro/e2e/server-islands.test.js
@@ -0,0 +1,59 @@
+import { expect } from '@playwright/test';
+import { testFactory } from './test-utils.js';
+
+const test = testFactory({ root: './fixtures/server-islands/' });
+
+test.describe('Server islands', () => {
+ test.describe('Development', () => {
+ let devServer;
+
+ test.beforeAll(async ({ astro }) => {
+ devServer = await astro.startDevServer();
+ });
+
+ test.afterAll(async () => {
+ await devServer.stop();
+ });
+
+ test('Load content from the server', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+ let el = page.locator('#island');
+
+ await expect(el, 'element rendered').toBeVisible();
+ await expect(el, 'should have content').toHaveText('I am an island');
+ });
+
+ test('Can be in an MDX file', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/mdx'));
+ let el = page.locator('#island');
+
+ await expect(el, 'element rendered').toBeVisible();
+ await expect(el, 'should have content').toHaveText('I am an island');
+ });
+ });
+
+ test.describe('Production', () => {
+ let previewServer;
+
+ test.beforeAll(async ({ astro }) => {
+ // Playwright's Node version doesn't have these functions, so stub them.
+ process.stdout.clearLine = () => {};
+ process.stdout.cursorTo = () => {};
+ await astro.build();
+ previewServer = await astro.preview();
+ });
+
+ test.afterAll(async () => {
+ await previewServer.stop();
+ });
+
+ test('Only one component in prod', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+
+ let el = page.locator('#island');
+
+ await expect(el, 'element rendered').toBeVisible();
+ await expect(el, 'should have content').toHaveText('I am an island');
+ });
+ });
+});
diff --git a/packages/astro/src/core/server-islands/endpoint.ts b/packages/astro/src/core/server-islands/endpoint.ts
index c9d3f5866d30..35063b0822b1 100644
--- a/packages/astro/src/core/server-islands/endpoint.ts
+++ b/packages/astro/src/core/server-islands/endpoint.ts
@@ -69,6 +69,7 @@ export function createEndpoint(manifest: SSRManifest) {
const instance: ComponentInstance = {
default: page,
+ partial: true,
};
return instance;
diff --git a/packages/astro/test/fixtures/server-islands/hybrid/astro.config.mjs b/packages/astro/test/fixtures/server-islands/hybrid/astro.config.mjs
new file mode 100644
index 000000000000..8a47dbb0292e
--- /dev/null
+++ b/packages/astro/test/fixtures/server-islands/hybrid/astro.config.mjs
@@ -0,0 +1,10 @@
+import svelte from '@astrojs/svelte';
+import { defineConfig } from 'astro/config';
+
+export default defineConfig({
+ output: 'hybrid',
+ integrations: [
+ svelte()
+ ]
+});
+
diff --git a/packages/astro/test/fixtures/server-islands/hybrid/package.json b/packages/astro/test/fixtures/server-islands/hybrid/package.json
new file mode 100644
index 000000000000..fdb447b0e071
--- /dev/null
+++ b/packages/astro/test/fixtures/server-islands/hybrid/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@test/server-islands-hybrid",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/svelte": "workspace:*",
+ "astro": "workspace:*",
+ "svelte": "^4.2.18"
+ }
+}
diff --git a/packages/astro/test/fixtures/server-islands/hybrid/src/components/Island.astro b/packages/astro/test/fixtures/server-islands/hybrid/src/components/Island.astro
new file mode 100644
index 000000000000..49a5a87ae0d5
--- /dev/null
+++ b/packages/astro/test/fixtures/server-islands/hybrid/src/components/Island.astro
@@ -0,0 +1,4 @@
+---
+
+---
+I'm an island
diff --git a/packages/astro/test/fixtures/server-islands/hybrid/src/pages/index.astro b/packages/astro/test/fixtures/server-islands/hybrid/src/pages/index.astro
new file mode 100644
index 000000000000..d42973294e6d
--- /dev/null
+++ b/packages/astro/test/fixtures/server-islands/hybrid/src/pages/index.astro
@@ -0,0 +1,12 @@
+---
+import Island from '../components/Island.astro';
+---
+
+
+ Testing
+
+
+ Testing
+
+
+
diff --git a/packages/astro/test/fixtures/server-islands/ssr/astro.config.mjs b/packages/astro/test/fixtures/server-islands/ssr/astro.config.mjs
new file mode 100644
index 000000000000..9d52f7a5fd09
--- /dev/null
+++ b/packages/astro/test/fixtures/server-islands/ssr/astro.config.mjs
@@ -0,0 +1,10 @@
+import svelte from '@astrojs/svelte';
+import { defineConfig } from 'astro/config';
+
+export default defineConfig({
+ output: 'server',
+ integrations: [
+ svelte()
+ ]
+});
+
diff --git a/packages/astro/test/fixtures/server-islands/ssr/package.json b/packages/astro/test/fixtures/server-islands/ssr/package.json
new file mode 100644
index 000000000000..fa6e000dda49
--- /dev/null
+++ b/packages/astro/test/fixtures/server-islands/ssr/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "@test/server-islands-ssr",
+ "version": "0.0.0",
+ "private": true,
+ "dependencies": {
+ "@astrojs/svelte": "workspace:*",
+ "astro": "workspace:*",
+ "svelte": "^4.2.18"
+ }
+}
diff --git a/packages/astro/test/fixtures/server-islands/ssr/src/components/Island.astro b/packages/astro/test/fixtures/server-islands/ssr/src/components/Island.astro
new file mode 100644
index 000000000000..49a5a87ae0d5
--- /dev/null
+++ b/packages/astro/test/fixtures/server-islands/ssr/src/components/Island.astro
@@ -0,0 +1,4 @@
+---
+
+---
+I'm an island
diff --git a/packages/astro/test/fixtures/server-islands/ssr/src/pages/index.astro b/packages/astro/test/fixtures/server-islands/ssr/src/pages/index.astro
new file mode 100644
index 000000000000..d42973294e6d
--- /dev/null
+++ b/packages/astro/test/fixtures/server-islands/ssr/src/pages/index.astro
@@ -0,0 +1,12 @@
+---
+import Island from '../components/Island.astro';
+---
+
+
+ Testing
+
+
+ Testing
+
+
+
diff --git a/packages/astro/test/server-islands.test.js b/packages/astro/test/server-islands.test.js
new file mode 100644
index 000000000000..60fece1e46d4
--- /dev/null
+++ b/packages/astro/test/server-islands.test.js
@@ -0,0 +1,120 @@
+
+import assert from 'node:assert/strict';
+import { after, before, describe, it } from 'node:test';
+import * as cheerio from 'cheerio';
+import testAdapter from './test-adapter.js';
+import { loadFixture } from './test-utils.js';
+
+describe('Server islands', () => {
+ describe('SSR', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/server-islands/ssr',
+ adapter: testAdapter(),
+ });
+ });
+
+ describe('dev', () => {
+ let devServer;
+
+ before(async () => {
+ devServer = await fixture.startDevServer();
+ });
+
+ after(async () => {
+ await devServer.stop();
+ });
+
+ it('omits the islands HTML', async () => {
+ const res = await fixture.fetch('/');
+ assert.equal(res.status, 200);
+ const html = await res.text();
+ const $ = cheerio.load(html);
+ const serverIslandEl = $('h2#island');
+ assert.equal(serverIslandEl.length, 0);
+ });
+ });
+
+ describe('prod', () => {
+ before(async () => {
+ await fixture.build();
+ });
+
+ it('omits the islands HTML', async () => {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/');
+ const response = await app.render(request);
+ const html = await response.text();
+
+ const $ = cheerio.load(html);
+ const serverIslandEl = $('h2#island');
+ assert.equal(serverIslandEl.length, 0);
+
+ const serverIslandScript = $('script[data-island-id]');
+ assert.equal(serverIslandScript.length, 1, 'has the island script');
+ });
+ });
+ });
+
+ describe('Hybrid mode', () => {
+ /** @type {import('./test-utils').Fixture} */
+ let fixture;
+ before(async () => {
+ fixture = await loadFixture({
+ root: './fixtures/server-islands/hybrid',
+ adapter: testAdapter(),
+ });
+ });
+
+ describe('build', () => {
+ before(async () => {
+ await fixture.build();
+ });
+
+ it('Omits the island HTML from the static HTML', async () => {
+ let html = await fixture.readFile('/client/index.html');
+
+ const $ = cheerio.load(html);
+ const serverIslandEl = $('h2#island');
+ assert.equal(serverIslandEl.length, 0);
+
+ const serverIslandScript = $('script[data-island-id]');
+ assert.equal(serverIslandScript.length, 1, 'has the island script');
+ });
+
+ describe('prod', () => {
+ async function fetchIsland() {
+ const app = await fixture.loadTestAdapterApp();
+ const request = new Request('http://example.com/_server-islands/Island', {
+ method: 'POST',
+ body: JSON.stringify({
+ componentExport: 'default',
+ props: {},
+ slots: {},
+ })
+ });
+ return app.render(request);
+ }
+
+ it('Island returns its HTML', async () => {
+ const response = await fetchIsland();
+ const html = await response.text();
+ const $ = cheerio.load(html);
+
+ const serverIslandEl = $('h2#island');
+ assert.equal(serverIslandEl.length, 1);
+ });
+
+ it('Island does not include the doctype', async () => {
+ const response = await fetchIsland();
+ const html = await response.text();
+ console.log(html);
+
+ assert.ok(!/doctype/i.test(html), 'html does not include doctype');
+ });
+ });
+ });
+ });
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6b3d759ba89a..8769f7a8f5c9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1596,6 +1596,27 @@ importers:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
+ packages/astro/e2e/fixtures/server-islands:
+ dependencies:
+ '@astrojs/mdx':
+ specifier: workspace:*
+ version: link:../../../../integrations/mdx
+ '@astrojs/node':
+ specifier: workspace:*
+ version: link:../../../../integrations/node
+ '@astrojs/react':
+ specifier: workspace:*
+ version: link:../../../../integrations/react
+ astro:
+ specifier: workspace:*
+ version: link:../../..
+ react:
+ specifier: ^18.3.1
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.3.1
+ version: 18.3.1(react@18.3.1)
+
packages/astro/e2e/fixtures/solid-circular:
dependencies:
'@astrojs/solid-js':
@@ -3616,6 +3637,30 @@ importers:
specifier: workspace:*
version: link:../../..
+ packages/astro/test/fixtures/server-islands/hybrid:
+ dependencies:
+ '@astrojs/svelte':
+ specifier: workspace:*
+ version: link:../../../../../integrations/svelte
+ astro:
+ specifier: workspace:*
+ version: link:../../../..
+ svelte:
+ specifier: ^4.2.18
+ version: 4.2.18
+
+ packages/astro/test/fixtures/server-islands/ssr:
+ dependencies:
+ '@astrojs/svelte':
+ specifier: workspace:*
+ version: link:../../../../../integrations/svelte
+ astro:
+ specifier: workspace:*
+ version: link:../../../..
+ svelte:
+ specifier: ^4.2.18
+ version: 4.2.18
+
packages/astro/test/fixtures/set-html:
dependencies:
astro: