diff --git a/.changeset/five-rocks-vanish.md b/.changeset/five-rocks-vanish.md
new file mode 100644
index 000000000000..7113deb2969b
--- /dev/null
+++ b/.changeset/five-rocks-vanish.md
@@ -0,0 +1,42 @@
+---
+'astro': minor
+---
+
+Experimental Server Islands
+
+Server Islands allow you to specify components that should run on the server, allowing the rest of the page to be more aggressively cached, or even generated statically. Turn any `.astro` component into a server island by adding the `server:defer` directive and optionally, fallback placeholder content:
+
+```astro
+---
+import Avatar from '../components/Avatar.astro';
+import GenericUser from '../components/GenericUser.astro';
+---
+
+
+```
+
+The `server:defer` directive can be used on any Astro component in a project using `hybrid` or `server` mode with an adapter. There are no special APIs needed inside of the island.
+
+Enable server islands by adding the experimental flag to your Astro config with an appropriate `output` mode and adatper:
+
+```js
+import { defineConfig } from 'astro/config';
+import netlify from '@astrojs/netlify';
+
+export default defineConfig({
+ output: 'hybrid',
+ adapter: netlify(),
+ experimental {
+ serverIslands: true,
+ },
+});
+```
+
+For more information, see the [server islands documentation](https://docs.astro.build/en/reference/configuration-reference/#experimentalserverislands).
diff --git a/examples/server-islands/astro.config.mjs b/examples/server-islands/astro.config.mjs
new file mode 100644
index 000000000000..c0d6b918a52e
--- /dev/null
+++ b/examples/server-islands/astro.config.mjs
@@ -0,0 +1,18 @@
+import { defineConfig } from 'astro/config';
+import nodejs from '@astrojs/node';
+import react from '@astrojs/react';
+import tailwind from '@astrojs/tailwind';
+
+// https://astro.build/config
+export default defineConfig({
+ output: 'server',
+ adapter: nodejs({ mode: 'standalone' }),
+ integrations: [
+ react(),
+ tailwind({ applyBaseStyles: false })
+ ],
+ devToolbar: { enabled: false },
+ experimental: {
+ serverIslands: true,
+ }
+});
diff --git a/examples/server-islands/package.json b/examples/server-islands/package.json
new file mode 100644
index 000000000000..b80361b5e65b
--- /dev/null
+++ b/examples/server-islands/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@example/server-islands",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "start": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro"
+ },
+ "devDependencies": {
+ "@astrojs/node": "^8.2.6",
+ "@astrojs/react": "^3.6.0",
+ "@astrojs/tailwind": "^5.1.0",
+ "@fortawesome/fontawesome-free": "^6.5.2",
+ "@tailwindcss/forms": "^0.5.7",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "astro": "workspace:*",
+ "postcss": "^8.4.38",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "tailwindcss": "^3.4.3"
+ }
+}
diff --git a/examples/server-islands/public/assets/css/main.css b/examples/server-islands/public/assets/css/main.css
new file mode 100644
index 000000000000..e30382e7c369
--- /dev/null
+++ b/examples/server-islands/public/assets/css/main.css
@@ -0,0 +1,1766 @@
+/* @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap");
+@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap"); */
+
+/* ! tailwindcss v3.0.23 | MIT License | https://tailwindcss.com */
+
+/*
+1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
+2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
+*/
+
+*,
+::before,
+::after {
+ box-sizing: border-box;
+ /* 1 */
+ border-width: 0;
+ /* 2 */
+ border-style: solid;
+ /* 2 */
+ border-color: #e5e7eb;
+ /* 2 */
+}
+
+::before,
+::after {
+ --tw-content: '';
+}
+
+/*
+1. Use a consistent sensible line-height in all browsers.
+2. Prevent adjustments of font size after orientation changes in iOS.
+3. Use a more readable tab size.
+4. Use the user's configured `sans` font-family by default.
+*/
+
+html {
+ line-height: 1.5;
+ /* 1 */
+ -webkit-text-size-adjust: 100%;
+ /* 2 */
+ -moz-tab-size: 4;
+ /* 3 */
+ -o-tab-size: 4;
+ tab-size: 4;
+ /* 3 */
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ /* 4 */
+}
+
+/*
+1. Remove the margin in all browsers.
+2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
+*/
+
+body {
+ margin: 0;
+ /* 1 */
+ line-height: inherit;
+ /* 2 */
+}
+
+/*
+1. Add the correct height in Firefox.
+2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
+3. Ensure horizontal rules are visible by default.
+*/
+
+hr {
+ height: 0;
+ /* 1 */
+ color: inherit;
+ /* 2 */
+ border-top-width: 1px;
+ /* 3 */
+}
+
+/*
+Add the correct text decoration in Chrome, Edge, and Safari.
+*/
+
+abbr:where([title]) {
+ -webkit-text-decoration: underline dotted;
+ text-decoration: underline dotted;
+}
+
+/*
+Remove the default font size and weight for headings.
+*/
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-size: inherit;
+ font-weight: inherit;
+}
+
+/*
+Reset links to optimize for opt-in styling instead of opt-out.
+*/
+
+a {
+ color: inherit;
+ text-decoration: inherit;
+}
+
+/*
+Add the correct font weight in Edge and Safari.
+*/
+
+b,
+strong {
+ font-weight: bolder;
+}
+
+/*
+1. Use the user's configured `mono` font family by default.
+2. Correct the odd `em` font sizing in all browsers.
+*/
+
+code,
+kbd,
+samp,
+pre {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ /* 1 */
+ font-size: 1em;
+ /* 2 */
+}
+
+/*
+Add the correct font size in all browsers.
+*/
+
+small {
+ font-size: 80%;
+}
+
+/*
+Prevent `sub` and `sup` elements from affecting the line height in all browsers.
+*/
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+/*
+1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
+2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
+3. Remove gaps between table borders by default.
+*/
+
+table {
+ text-indent: 0;
+ /* 1 */
+ border-color: inherit;
+ /* 2 */
+ border-collapse: collapse;
+ /* 3 */
+}
+
+/*
+1. Change the font styles in all browsers.
+2. Remove the margin in Firefox and Safari.
+3. Remove default padding in all browsers.
+*/
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: inherit;
+ /* 1 */
+ font-size: 100%;
+ /* 1 */
+ line-height: inherit;
+ /* 1 */
+ color: inherit;
+ /* 1 */
+ margin: 0;
+ /* 2 */
+ padding: 0;
+ /* 3 */
+}
+
+/*
+Remove the inheritance of text transform in Edge and Firefox.
+*/
+
+button,
+select {
+ text-transform: none;
+}
+
+/*
+1. Correct the inability to style clickable types in iOS and Safari.
+2. Remove default button styles.
+*/
+
+button,
+[type='button'],
+[type='reset'],
+[type='submit'] {
+ -webkit-appearance: button;
+ /* 1 */
+ background-color: transparent;
+ /* 2 */
+ background-image: none;
+ /* 2 */
+}
+
+/*
+Use the modern Firefox focus style for all focusable elements.
+*/
+
+:-moz-focusring {
+ outline: auto;
+}
+
+/*
+Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
+*/
+
+:-moz-ui-invalid {
+ box-shadow: none;
+}
+
+/*
+Add the correct vertical alignment in Chrome and Firefox.
+*/
+
+progress {
+ vertical-align: baseline;
+}
+
+/*
+Correct the cursor style of increment and decrement buttons in Safari.
+*/
+
+::-webkit-inner-spin-button,
+::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/*
+1. Correct the odd appearance in Chrome and Safari.
+2. Correct the outline style in Safari.
+*/
+
+[type='search'] {
+ -webkit-appearance: textfield;
+ /* 1 */
+ outline-offset: -2px;
+ /* 2 */
+}
+
+/*
+Remove the inner padding in Chrome and Safari on macOS.
+*/
+
+::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/*
+1. Correct the inability to style clickable types in iOS and Safari.
+2. Change font properties to `inherit` in Safari.
+*/
+
+::-webkit-file-upload-button {
+ -webkit-appearance: button;
+ /* 1 */
+ font: inherit;
+ /* 2 */
+}
+
+/*
+Add the correct display in Chrome and Safari.
+*/
+
+summary {
+ display: list-item;
+}
+
+/*
+Removes the default spacing and border for appropriate elements.
+*/
+
+blockquote,
+dl,
+dd,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+hr,
+figure,
+p,
+pre {
+ margin: 0;
+}
+
+fieldset {
+ margin: 0;
+ padding: 0;
+}
+
+legend {
+ padding: 0;
+}
+
+ol,
+ul,
+menu {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+/*
+Prevent resizing textareas horizontally by default.
+*/
+
+textarea {
+ resize: vertical;
+}
+
+/*
+1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
+2. Set the default placeholder color to the user's configured gray 400 color.
+*/
+
+input::-moz-placeholder, textarea::-moz-placeholder {
+ opacity: 1;
+ /* 1 */
+ color: #9ca3af;
+ /* 2 */
+}
+
+input:-ms-input-placeholder, textarea:-ms-input-placeholder {
+ opacity: 1;
+ /* 1 */
+ color: #9ca3af;
+ /* 2 */
+}
+
+input::placeholder,
+textarea::placeholder {
+ opacity: 1;
+ /* 1 */
+ color: #9ca3af;
+ /* 2 */
+}
+
+/*
+Set the default cursor for buttons.
+*/
+
+button,
+[role="button"] {
+ cursor: pointer;
+}
+
+/*
+Make sure disabled buttons don't get the pointer cursor.
+*/
+
+:disabled {
+ cursor: default;
+}
+
+/*
+1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
+2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
+ This can trigger a poorly considered lint error in some tools but is included by design.
+*/
+
+img,
+svg,
+video,
+canvas,
+audio,
+iframe,
+embed,
+object {
+ display: block;
+ /* 1 */
+ vertical-align: middle;
+ /* 2 */
+}
+
+/*
+Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
+*/
+
+img,
+video {
+ max-width: 100%;
+ height: auto;
+}
+
+/*
+Ensure the default browser behavior of the `hidden` attribute.
+*/
+
+[hidden] {
+ display: none;
+}
+
+[type='text'],[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background-color: #fff;
+ border-color: #6b7280;
+ border-width: 1px;
+ border-radius: 0px;
+ padding-top: 0.5rem;
+ padding-right: 0.75rem;
+ padding-bottom: 0.5rem;
+ padding-left: 0.75rem;
+ font-size: 1rem;
+ line-height: 1.5rem;
+ --tw-shadow: 0 0 #0000;
+}
+
+[type='text']:focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: #2563eb;
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+ border-color: #2563eb;
+}
+
+input::-moz-placeholder, textarea::-moz-placeholder {
+ color: #6b7280;
+ opacity: 1;
+}
+
+input:-ms-input-placeholder, textarea:-ms-input-placeholder {
+ color: #6b7280;
+ opacity: 1;
+}
+
+input::placeholder,textarea::placeholder {
+ color: #6b7280;
+ opacity: 1;
+}
+
+::-webkit-datetime-edit-fields-wrapper {
+ padding: 0;
+}
+
+::-webkit-date-and-time-value {
+ min-height: 1.5em;
+}
+
+::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field {
+ padding-top: 0;
+ padding-bottom: 0;
+}
+
+select {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
+ background-position: right 0.5rem center;
+ background-repeat: no-repeat;
+ background-size: 1.5em 1.5em;
+ padding-right: 2.5rem;
+ -webkit-print-color-adjust: exact;
+ color-adjust: exact;
+}
+
+[multiple] {
+ background-image: initial;
+ background-position: initial;
+ background-repeat: unset;
+ background-size: initial;
+ padding-right: 0.75rem;
+ -webkit-print-color-adjust: unset;
+ color-adjust: unset;
+}
+
+[type='checkbox'],[type='radio'] {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ padding: 0;
+ -webkit-print-color-adjust: exact;
+ color-adjust: exact;
+ display: inline-block;
+ vertical-align: middle;
+ background-origin: border-box;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ flex-shrink: 0;
+ height: 1rem;
+ width: 1rem;
+ color: #2563eb;
+ background-color: #fff;
+ border-color: #6b7280;
+ border-width: 1px;
+ --tw-shadow: 0 0 #0000;
+}
+
+[type='checkbox'] {
+ border-radius: 0px;
+}
+
+[type='radio'] {
+ border-radius: 100%;
+}
+
+[type='checkbox']:focus,[type='radio']:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
+ --tw-ring-offset-width: 2px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: #2563eb;
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
+}
+
+[type='checkbox']:checked,[type='radio']:checked {
+ border-color: transparent;
+ background-color: currentColor;
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+[type='checkbox']:checked {
+ background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
+}
+
+[type='radio']:checked {
+ background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e");
+}
+
+[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus {
+ border-color: transparent;
+ background-color: currentColor;
+}
+
+[type='checkbox']:indeterminate {
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e");
+ border-color: transparent;
+ background-color: currentColor;
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus {
+ border-color: transparent;
+ background-color: currentColor;
+}
+
+[type='file'] {
+ background: unset;
+ border-color: inherit;
+ border-width: 0;
+ border-radius: 0;
+ padding: 0;
+ font-size: unset;
+ line-height: inherit;
+}
+
+[type='file']:focus {
+ outline: 1px auto -webkit-focus-ring-color;
+}
+
+body {
+ font-family: Poppins, sans-serif;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-family: Roboto, sans-serif;
+}
+
+*, ::before, ::after {
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia: ;
+}
+
+.container {
+ width: 100%;
+ margin-right: auto;
+ margin-left: auto;
+ padding-right: 1rem;
+ padding-left: 1rem;
+}
+
+@media (min-width: 640px) {
+ .container {
+ max-width: 640px;
+ }
+}
+
+@media (min-width: 768px) {
+ .container {
+ max-width: 768px;
+ }
+}
+
+@media (min-width: 1024px) {
+ .container {
+ max-width: 1024px;
+ }
+}
+
+@media (min-width: 1280px) {
+ .container {
+ max-width: 1280px;
+ }
+}
+
+@media (min-width: 1536px) {
+ .container {
+ max-width: 1536px;
+ }
+}
+
+.size-selector input:checked + label {
+ --tw-bg-opacity: 1;
+ background-color: rgb(253 61 87 / var(--tw-bg-opacity));
+ --tw-text-opacity: 1;
+ color: rgb(255 255 255 / var(--tw-text-opacity));
+}
+
+.color-selector input:checked + label {
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(253 61 87 / var(--tw-ring-opacity));
+}
+
+.input-box {
+ display: block;
+ width: 100%;
+ border-radius: 0.25rem;
+ border-width: 1px;
+ --tw-border-opacity: 1;
+ border-color: rgb(209 213 219 / var(--tw-border-opacity));
+ padding-left: 1rem;
+ padding-right: 1rem;
+ padding-top: 0.75rem;
+ padding-bottom: 0.75rem;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ --tw-text-opacity: 1;
+ color: rgb(75 85 99 / var(--tw-text-opacity));
+}
+
+.input-box::-moz-placeholder {
+ --tw-placeholder-opacity: 1;
+ color: rgb(156 163 175 / var(--tw-placeholder-opacity));
+}
+
+.input-box:-ms-input-placeholder {
+ --tw-placeholder-opacity: 1;
+ color: rgb(156 163 175 / var(--tw-placeholder-opacity));
+}
+
+.input-box::placeholder {
+ --tw-placeholder-opacity: 1;
+ color: rgb(156 163 175 / var(--tw-placeholder-opacity));
+}
+
+.input-box:focus {
+ --tw-border-opacity: 1;
+ border-color: rgb(253 61 87 / var(--tw-border-opacity));
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+}
+
+.invisible {
+ visibility: hidden;
+}
+
+.absolute {
+ position: absolute;
+}
+
+.relative {
+ position: relative;
+}
+
+.inset-0 {
+ top: 0px;
+ right: 0px;
+ bottom: 0px;
+ left: 0px;
+}
+
+.left-4 {
+ left: 1rem;
+}
+
+.top-3 {
+ top: 0.75rem;
+}
+
+.right-0 {
+ right: 0px;
+}
+
+.-top-1 {
+ top: -0.25rem;
+}
+
+.-right-3 {
+ right: -0.75rem;
+}
+
+.left-0 {
+ left: 0px;
+}
+
+.top-full {
+ top: 100%;
+}
+
+.-left-8 {
+ left: -2rem;
+}
+
+.top-0 {
+ top: 0px;
+}
+
+.z-10 {
+ z-index: 10;
+}
+
+.col-span-1 {
+ grid-column: span 1 / span 1;
+}
+
+.col-span-2 {
+ grid-column: span 2 / span 2;
+}
+
+.col-span-3 {
+ grid-column: span 3 / span 3;
+}
+
+.col-span-9 {
+ grid-column: span 9 / span 9;
+}
+
+.col-span-8 {
+ grid-column: span 8 / span 8;
+}
+
+.col-span-4 {
+ grid-column: span 4 / span 4;
+}
+
+.mx-auto {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.mx-3 {
+ margin-left: 0.75rem;
+ margin-right: 0.75rem;
+}
+
+.ml-2 {
+ margin-left: 0.5rem;
+}
+
+.ml-6 {
+ margin-left: 1.5rem;
+}
+
+.mb-4 {
+ margin-bottom: 1rem;
+}
+
+.mt-12 {
+ margin-top: 3rem;
+}
+
+.mb-6 {
+ margin-bottom: 1.5rem;
+}
+
+.mb-2 {
+ margin-bottom: 0.5rem;
+}
+
+.mb-1 {
+ margin-bottom: 0.25rem;
+}
+
+.ml-3 {
+ margin-left: 0.75rem;
+}
+
+.mr-2 {
+ margin-right: 0.5rem;
+}
+
+.mt-4 {
+ margin-top: 1rem;
+}
+
+.mt-6 {
+ margin-top: 1.5rem;
+}
+
+.mt-1 {
+ margin-top: 0.25rem;
+}
+
+.mt-2 {
+ margin-top: 0.5rem;
+}
+
+.mb-3 {
+ margin-bottom: 0.75rem;
+}
+
+.ml-auto {
+ margin-left: auto;
+}
+
+.block {
+ display: block;
+}
+
+.flex {
+ display: flex;
+}
+
+.table {
+ display: table;
+}
+
+.grid {
+ display: grid;
+}
+
+.hidden {
+ display: none;
+}
+
+.h-5 {
+ height: 1.25rem;
+}
+
+.h-12 {
+ height: 3rem;
+}
+
+.h-8 {
+ height: 2rem;
+}
+
+.h-14 {
+ height: 3.5rem;
+}
+
+.h-3 {
+ height: 0.75rem;
+}
+
+.h-6 {
+ height: 1.5rem;
+}
+
+.h-9 {
+ height: 2.25rem;
+}
+
+.w-32 {
+ width: 8rem;
+}
+
+.w-full {
+ width: 100%;
+}
+
+.w-5 {
+ width: 1.25rem;
+}
+
+.w-10\/12 {
+ width: 83.333333%;
+}
+
+.w-12 {
+ width: 3rem;
+}
+
+.w-9 {
+ width: 2.25rem;
+}
+
+.w-14 {
+ width: 3.5rem;
+}
+
+.w-3 {
+ width: 0.75rem;
+}
+
+.w-1\/2 {
+ width: 50%;
+}
+
+.w-6 {
+ width: 1.5rem;
+}
+
+.w-max {
+ width: -webkit-max-content;
+ width: -moz-max-content;
+ width: max-content;
+}
+
+.w-8 {
+ width: 2rem;
+}
+
+.w-3\/5 {
+ width: 60%;
+}
+
+.w-40 {
+ width: 10rem;
+}
+
+.w-44 {
+ width: 11rem;
+}
+
+.w-10 {
+ width: 2.5rem;
+}
+
+.w-28 {
+ width: 7rem;
+}
+
+.w-1\/3 {
+ width: 33.333333%;
+}
+
+.max-w-xl {
+ max-width: 36rem;
+}
+
+.max-w-lg {
+ max-width: 32rem;
+}
+
+.flex-shrink-0 {
+ flex-shrink: 0;
+}
+
+.flex-grow {
+ flex-grow: 1;
+}
+
+.table-auto {
+ table-layout: auto;
+}
+
+.border-collapse {
+ border-collapse: collapse;
+}
+
+.cursor-pointer {
+ cursor: pointer;
+}
+
+.cursor-not-allowed {
+ cursor: not-allowed;
+}
+
+.select-none {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.grid-cols-1 {
+ grid-template-columns: repeat(1, minmax(0, 1fr));
+}
+
+.grid-cols-3 {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+}
+
+.grid-cols-2 {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.grid-cols-12 {
+ grid-template-columns: repeat(12, minmax(0, 1fr));
+}
+
+.grid-cols-5 {
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+}
+
+.grid-cols-4 {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+
+.items-start {
+ align-items: flex-start;
+}
+
+.items-center {
+ align-items: center;
+}
+
+.items-baseline {
+ align-items: baseline;
+}
+
+.justify-center {
+ justify-content: center;
+}
+
+.justify-between {
+ justify-content: space-between;
+}
+
+.gap-6 {
+ gap: 1.5rem;
+}
+
+.gap-5 {
+ gap: 1.25rem;
+}
+
+.gap-3 {
+ gap: 0.75rem;
+}
+
+.gap-2 {
+ gap: 0.5rem;
+}
+
+.gap-1 {
+ gap: 0.25rem;
+}
+
+.gap-4 {
+ gap: 1rem;
+}
+
+.gap-8 {
+ gap: 2rem;
+}
+
+.space-x-4 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-x-reverse: 0;
+ margin-right: calc(1rem * var(--tw-space-x-reverse));
+ margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
+}
+
+.space-x-6 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-x-reverse: 0;
+ margin-right: calc(1.5rem * var(--tw-space-x-reverse));
+ margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse)));
+}
+
+.space-x-2 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-x-reverse: 0;
+ margin-right: calc(0.5rem * var(--tw-space-x-reverse));
+ margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
+}
+
+.space-y-4 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(1rem * var(--tw-space-y-reverse));
+}
+
+.space-x-5 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-x-reverse: 0;
+ margin-right: calc(1.25rem * var(--tw-space-x-reverse));
+ margin-left: calc(1.25rem * calc(1 - var(--tw-space-x-reverse)));
+}
+
+.space-y-1 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
+}
+
+.space-y-8 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(2rem * var(--tw-space-y-reverse));
+}
+
+.space-y-2 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
+}
+
+.space-y-5 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(1.25rem * var(--tw-space-y-reverse));
+}
+
+.divide-y > :not([hidden]) ~ :not([hidden]) {
+ --tw-divide-y-reverse: 0;
+ border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
+ border-bottom-width: calc(1px * var(--tw-divide-y-reverse));
+}
+
+.divide-x > :not([hidden]) ~ :not([hidden]) {
+ --tw-divide-x-reverse: 0;
+ border-right-width: calc(1px * var(--tw-divide-x-reverse));
+ border-left-width: calc(1px * calc(1 - var(--tw-divide-x-reverse)));
+}
+
+.divide-dashed > :not([hidden]) ~ :not([hidden]) {
+ border-style: dashed;
+}
+
+.divide-gray-300 > :not([hidden]) ~ :not([hidden]) {
+ --tw-divide-opacity: 1;
+ border-color: rgb(209 213 219 / var(--tw-divide-opacity));
+}
+
+.divide-gray-200 > :not([hidden]) ~ :not([hidden]) {
+ --tw-divide-opacity: 1;
+ border-color: rgb(229 231 235 / var(--tw-divide-opacity));
+}
+
+.overflow-hidden {
+ overflow: hidden;
+}
+
+.rounded-full {
+ border-radius: 9999px;
+}
+
+.rounded-md {
+ border-radius: 0.375rem;
+}
+
+.rounded-sm {
+ border-radius: 0.125rem;
+}
+
+.rounded {
+ border-radius: 0.25rem;
+}
+
+.rounded-l-md {
+ border-top-left-radius: 0.375rem;
+ border-bottom-left-radius: 0.375rem;
+}
+
+.rounded-r-md {
+ border-top-right-radius: 0.375rem;
+ border-bottom-right-radius: 0.375rem;
+}
+
+.rounded-b {
+ border-bottom-right-radius: 0.25rem;
+ border-bottom-left-radius: 0.25rem;
+}
+
+.border {
+ border-width: 1px;
+}
+
+.border-r-0 {
+ border-right-width: 0px;
+}
+
+.border-t {
+ border-top-width: 1px;
+}
+
+.border-b {
+ border-bottom-width: 1px;
+}
+
+.border-b-2 {
+ border-bottom-width: 2px;
+}
+
+.border-primary {
+ --tw-border-opacity: 1;
+ border-color: rgb(253 61 87 / var(--tw-border-opacity));
+}
+
+.border-gray-100 {
+ --tw-border-opacity: 1;
+ border-color: rgb(243 244 246 / var(--tw-border-opacity));
+}
+
+.border-gray-200 {
+ --tw-border-opacity: 1;
+ border-color: rgb(229 231 235 / var(--tw-border-opacity));
+}
+
+.border-gray-300 {
+ --tw-border-opacity: 1;
+ border-color: rgb(209 213 219 / var(--tw-border-opacity));
+}
+
+.border-red-400 {
+ --tw-border-opacity: 1;
+ border-color: rgb(248 113 113 / var(--tw-border-opacity));
+}
+
+.bg-white {
+ --tw-bg-opacity: 1;
+ background-color: rgb(255 255 255 / var(--tw-bg-opacity));
+}
+
+.bg-primary {
+ --tw-bg-opacity: 1;
+ background-color: rgb(253 61 87 / var(--tw-bg-opacity));
+}
+
+.bg-gray-800 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(31 41 55 / var(--tw-bg-opacity));
+}
+
+.bg-black {
+ --tw-bg-opacity: 1;
+ background-color: rgb(0 0 0 / var(--tw-bg-opacity));
+}
+
+.bg-blue-800 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(30 64 175 / var(--tw-bg-opacity));
+}
+
+.bg-red-600 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(220 38 38 / var(--tw-bg-opacity));
+}
+
+.bg-red-400 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(248 113 113 / var(--tw-bg-opacity));
+}
+
+.bg-opacity-40 {
+ --tw-bg-opacity: 0.4;
+}
+
+.bg-cover {
+ background-size: cover;
+}
+
+.bg-center {
+ background-position: center;
+}
+
+.bg-no-repeat {
+ background-repeat: no-repeat;
+}
+
+.object-contain {
+ -o-object-fit: contain;
+ object-fit: contain;
+}
+
+.object-cover {
+ -o-object-fit: cover;
+ object-fit: cover;
+}
+
+.p-1 {
+ padding: 0.25rem;
+}
+
+.p-4 {
+ padding: 1rem;
+}
+
+.py-4 {
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+}
+
+.py-3 {
+ padding-top: 0.75rem;
+ padding-bottom: 0.75rem;
+}
+
+.px-8 {
+ padding-left: 2rem;
+ padding-right: 2rem;
+}
+
+.px-6 {
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
+}
+
+.py-5 {
+ padding-top: 1.25rem;
+ padding-bottom: 1.25rem;
+}
+
+.py-36 {
+ padding-top: 9rem;
+ padding-bottom: 9rem;
+}
+
+.py-16 {
+ padding-top: 4rem;
+ padding-bottom: 4rem;
+}
+
+.px-3 {
+ padding-left: 0.75rem;
+ padding-right: 0.75rem;
+}
+
+.py-6 {
+ padding-top: 1.5rem;
+ padding-bottom: 1.5rem;
+}
+
+.px-4 {
+ padding-left: 1rem;
+ padding-right: 1rem;
+}
+
+.py-1 {
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+}
+
+.py-7 {
+ padding-top: 1.75rem;
+ padding-bottom: 1.75rem;
+}
+
+.py-2 {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+}
+
+.pl-12 {
+ padding-left: 3rem;
+}
+
+.pr-3 {
+ padding-right: 0.75rem;
+}
+
+.pb-16 {
+ padding-bottom: 4rem;
+}
+
+.pt-4 {
+ padding-top: 1rem;
+}
+
+.pb-3 {
+ padding-bottom: 0.75rem;
+}
+
+.pt-16 {
+ padding-top: 4rem;
+}
+
+.pb-12 {
+ padding-bottom: 3rem;
+}
+
+.pl-8 {
+ padding-left: 2rem;
+}
+
+.pt-6 {
+ padding-top: 1.5rem;
+}
+
+.pb-8 {
+ padding-bottom: 2rem;
+}
+
+.pb-5 {
+ padding-bottom: 1.25rem;
+}
+
+.pt-5 {
+ padding-top: 1.25rem;
+}
+
+.pb-7 {
+ padding-bottom: 1.75rem;
+}
+
+.pb-6 {
+ padding-bottom: 1.5rem;
+}
+
+.text-left {
+ text-align: left;
+}
+
+.text-center {
+ text-align: center;
+}
+
+.font-roboto {
+ font-family: Roboto, sans-serif;
+}
+
+.text-lg {
+ font-size: 1.125rem;
+ line-height: 1.75rem;
+}
+
+.text-2xl {
+ font-size: 1.5rem;
+ line-height: 2rem;
+}
+
+.text-xs {
+ font-size: 0.75rem;
+ line-height: 1rem;
+}
+
+.text-sm {
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+}
+
+.text-6xl {
+ font-size: 3.75rem;
+ line-height: 1;
+}
+
+.text-xl {
+ font-size: 1.25rem;
+ line-height: 1.75rem;
+}
+
+.text-base {
+ font-size: 1rem;
+ line-height: 1.5rem;
+}
+
+.text-3xl {
+ font-size: 1.875rem;
+ line-height: 2.25rem;
+}
+
+.font-medium {
+ font-weight: 500;
+}
+
+.font-semibold {
+ font-weight: 600;
+}
+
+.uppercase {
+ text-transform: uppercase;
+}
+
+.capitalize {
+ text-transform: capitalize;
+}
+
+.leading-3 {
+ line-height: .75rem;
+}
+
+.tracking-wider {
+ letter-spacing: 0.05em;
+}
+
+.text-gray-400 {
+ --tw-text-opacity: 1;
+ color: rgb(156 163 175 / var(--tw-text-opacity));
+}
+
+.text-white {
+ --tw-text-opacity: 1;
+ color: rgb(255 255 255 / var(--tw-text-opacity));
+}
+
+.text-gray-700 {
+ --tw-text-opacity: 1;
+ color: rgb(55 65 81 / var(--tw-text-opacity));
+}
+
+.text-gray-600 {
+ --tw-text-opacity: 1;
+ color: rgb(75 85 99 / var(--tw-text-opacity));
+}
+
+.text-gray-200 {
+ --tw-text-opacity: 1;
+ color: rgb(229 231 235 / var(--tw-text-opacity));
+}
+
+.text-gray-800 {
+ --tw-text-opacity: 1;
+ color: rgb(31 41 55 / var(--tw-text-opacity));
+}
+
+.text-gray-500 {
+ --tw-text-opacity: 1;
+ color: rgb(107 114 128 / var(--tw-text-opacity));
+}
+
+.text-primary {
+ --tw-text-opacity: 1;
+ color: rgb(253 61 87 / var(--tw-text-opacity));
+}
+
+.text-yellow-400 {
+ --tw-text-opacity: 1;
+ color: rgb(250 204 21 / var(--tw-text-opacity));
+}
+
+.text-green-600 {
+ --tw-text-opacity: 1;
+ color: rgb(22 163 74 / var(--tw-text-opacity));
+}
+
+.text-red-600 {
+ --tw-text-opacity: 1;
+ color: rgb(220 38 38 / var(--tw-text-opacity));
+}
+
+.line-through {
+ -webkit-text-decoration-line: line-through;
+ text-decoration-line: line-through;
+}
+
+.placeholder-gray-400::-moz-placeholder {
+ --tw-placeholder-opacity: 1;
+ color: rgb(156 163 175 / var(--tw-placeholder-opacity));
+}
+
+.placeholder-gray-400:-ms-input-placeholder {
+ --tw-placeholder-opacity: 1;
+ color: rgb(156 163 175 / var(--tw-placeholder-opacity));
+}
+
+.placeholder-gray-400::placeholder {
+ --tw-placeholder-opacity: 1;
+ color: rgb(156 163 175 / var(--tw-placeholder-opacity));
+}
+
+.opacity-0 {
+ opacity: 0;
+}
+
+.shadow-sm {
+ --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+ --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
+.shadow-md {
+ --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
+ --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
+.shadow {
+ --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
+ --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
+.transition {
+ transition-property: color, background-color, border-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-text-decoration-color, -webkit-backdrop-filter;
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-text-decoration-color, -webkit-backdrop-filter;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.duration-300 {
+ transition-duration: 300ms;
+}
+
+.hover\:bg-transparent:hover {
+ background-color: transparent;
+}
+
+.hover\:bg-gray-100:hover {
+ --tw-bg-opacity: 1;
+ background-color: rgb(243 244 246 / var(--tw-bg-opacity));
+}
+
+.hover\:bg-gray-800:hover {
+ --tw-bg-opacity: 1;
+ background-color: rgb(31 41 55 / var(--tw-bg-opacity));
+}
+
+.hover\:bg-blue-700:hover {
+ --tw-bg-opacity: 1;
+ background-color: rgb(29 78 216 / var(--tw-bg-opacity));
+}
+
+.hover\:bg-red-500:hover {
+ --tw-bg-opacity: 1;
+ background-color: rgb(239 68 68 / var(--tw-bg-opacity));
+}
+
+.hover\:text-primary:hover {
+ --tw-text-opacity: 1;
+ color: rgb(253 61 87 / var(--tw-text-opacity));
+}
+
+.hover\:text-white:hover {
+ --tw-text-opacity: 1;
+ color: rgb(255 255 255 / var(--tw-text-opacity));
+}
+
+.hover\:text-gray-500:hover {
+ --tw-text-opacity: 1;
+ color: rgb(107 114 128 / var(--tw-text-opacity));
+}
+
+.hover\:text-gray-900:hover {
+ --tw-text-opacity: 1;
+ color: rgb(17 24 39 / var(--tw-text-opacity));
+}
+
+.focus\:border-primary:focus {
+ --tw-border-opacity: 1;
+ border-color: rgb(253 61 87 / var(--tw-border-opacity));
+}
+
+.focus\:outline-none:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+}
+
+.focus\:ring-0:focus {
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+}
+
+.focus\:ring-primary:focus {
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(253 61 87 / var(--tw-ring-opacity));
+}
+
+.group:hover .group-hover\:visible {
+ visibility: visible;
+}
+
+.group:hover .group-hover\:bg-opacity-60 {
+ --tw-bg-opacity: 0.6;
+}
+
+.group:hover .group-hover\:opacity-100 {
+ opacity: 1;
+}
+
+@media (min-width: 768px) {
+ .md\:block {
+ display: block;
+ }
+
+ .md\:flex {
+ display: flex;
+ }
+
+ .md\:grid-cols-3 {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+
+ .md\:grid-cols-4 {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ }
+
+ .md\:gap-8 {
+ gap: 2rem;
+ }
+
+ .md\:pl-12 {
+ padding-left: 3rem;
+ }
+}
diff --git a/examples/server-islands/public/assets/images/avatar.png b/examples/server-islands/public/assets/images/avatar.png
new file mode 100644
index 000000000000..4e8512382cdd
Binary files /dev/null and b/examples/server-islands/public/assets/images/avatar.png differ
diff --git a/examples/server-islands/public/assets/images/banner-bg.jpg b/examples/server-islands/public/assets/images/banner-bg.jpg
new file mode 100644
index 000000000000..0ce119a45584
Binary files /dev/null and b/examples/server-islands/public/assets/images/banner-bg.jpg differ
diff --git a/examples/server-islands/public/assets/images/category/category-1.jpg b/examples/server-islands/public/assets/images/category/category-1.jpg
new file mode 100644
index 000000000000..e45bf4ae01ec
Binary files /dev/null and b/examples/server-islands/public/assets/images/category/category-1.jpg differ
diff --git a/examples/server-islands/public/assets/images/category/category-2.jpg b/examples/server-islands/public/assets/images/category/category-2.jpg
new file mode 100644
index 000000000000..56249e6a3f7d
Binary files /dev/null and b/examples/server-islands/public/assets/images/category/category-2.jpg differ
diff --git a/examples/server-islands/public/assets/images/category/category-3.jpg b/examples/server-islands/public/assets/images/category/category-3.jpg
new file mode 100644
index 000000000000..5ef6659465e9
Binary files /dev/null and b/examples/server-islands/public/assets/images/category/category-3.jpg differ
diff --git a/examples/server-islands/public/assets/images/category/category-4.jpg b/examples/server-islands/public/assets/images/category/category-4.jpg
new file mode 100644
index 000000000000..b12e648303c9
Binary files /dev/null and b/examples/server-islands/public/assets/images/category/category-4.jpg differ
diff --git a/examples/server-islands/public/assets/images/category/category-5.jpg b/examples/server-islands/public/assets/images/category/category-5.jpg
new file mode 100644
index 000000000000..f8e804f8c066
Binary files /dev/null and b/examples/server-islands/public/assets/images/category/category-5.jpg differ
diff --git a/examples/server-islands/public/assets/images/category/category-6.jpg b/examples/server-islands/public/assets/images/category/category-6.jpg
new file mode 100644
index 000000000000..e1991d35cbbd
Binary files /dev/null and b/examples/server-islands/public/assets/images/category/category-6.jpg differ
diff --git a/examples/server-islands/public/assets/images/complete.png b/examples/server-islands/public/assets/images/complete.png
new file mode 100644
index 000000000000..ea3fe9681e09
Binary files /dev/null and b/examples/server-islands/public/assets/images/complete.png differ
diff --git a/examples/server-islands/public/assets/images/favicon/about.txt b/examples/server-islands/public/assets/images/favicon/about.txt
new file mode 100644
index 000000000000..7229927f7072
--- /dev/null
+++ b/examples/server-islands/public/assets/images/favicon/about.txt
@@ -0,0 +1,6 @@
+This favicon was generated using the following font:
+
+- Font Title: Roboto
+- Font Author: Copyright 2011 Google Inc. All Rights Reserved.
+- Font Source: http://fonts.gstatic.com/s/roboto/v29/KFOlCnqEu92Fr1MmWUlvAx05IsDqlA.ttf
+- Font License: Apache License, version 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html))
diff --git a/examples/server-islands/public/assets/images/favicon/android-chrome-192x192.png b/examples/server-islands/public/assets/images/favicon/android-chrome-192x192.png
new file mode 100644
index 000000000000..6809c797894d
Binary files /dev/null and b/examples/server-islands/public/assets/images/favicon/android-chrome-192x192.png differ
diff --git a/examples/server-islands/public/assets/images/favicon/android-chrome-512x512.png b/examples/server-islands/public/assets/images/favicon/android-chrome-512x512.png
new file mode 100644
index 000000000000..27d7db79c6a0
Binary files /dev/null and b/examples/server-islands/public/assets/images/favicon/android-chrome-512x512.png differ
diff --git a/examples/server-islands/public/assets/images/favicon/apple-touch-icon.png b/examples/server-islands/public/assets/images/favicon/apple-touch-icon.png
new file mode 100644
index 000000000000..ff578ae53911
Binary files /dev/null and b/examples/server-islands/public/assets/images/favicon/apple-touch-icon.png differ
diff --git a/examples/server-islands/public/assets/images/favicon/favicon-16x16.png b/examples/server-islands/public/assets/images/favicon/favicon-16x16.png
new file mode 100644
index 000000000000..470a85966941
Binary files /dev/null and b/examples/server-islands/public/assets/images/favicon/favicon-16x16.png differ
diff --git a/examples/server-islands/public/assets/images/favicon/favicon-32x32.png b/examples/server-islands/public/assets/images/favicon/favicon-32x32.png
new file mode 100644
index 000000000000..56afd053d926
Binary files /dev/null and b/examples/server-islands/public/assets/images/favicon/favicon-32x32.png differ
diff --git a/examples/server-islands/public/assets/images/favicon/favicon.ico b/examples/server-islands/public/assets/images/favicon/favicon.ico
new file mode 100644
index 000000000000..77bb5fa1839f
Binary files /dev/null and b/examples/server-islands/public/assets/images/favicon/favicon.ico differ
diff --git a/examples/server-islands/public/assets/images/favicon/site.webmanifest b/examples/server-islands/public/assets/images/favicon/site.webmanifest
new file mode 100644
index 000000000000..45dc8a20658b
--- /dev/null
+++ b/examples/server-islands/public/assets/images/favicon/site.webmanifest
@@ -0,0 +1 @@
+{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
\ No newline at end of file
diff --git a/examples/server-islands/public/assets/images/icons/bed-2.svg b/examples/server-islands/public/assets/images/icons/bed-2.svg
new file mode 100644
index 000000000000..09b790da78db
--- /dev/null
+++ b/examples/server-islands/public/assets/images/icons/bed-2.svg
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/server-islands/public/assets/images/icons/bed.svg b/examples/server-islands/public/assets/images/icons/bed.svg
new file mode 100644
index 000000000000..d66adb50136e
--- /dev/null
+++ b/examples/server-islands/public/assets/images/icons/bed.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/server-islands/public/assets/images/icons/delivery-van.svg b/examples/server-islands/public/assets/images/icons/delivery-van.svg
new file mode 100644
index 000000000000..a6c08a447c8c
--- /dev/null
+++ b/examples/server-islands/public/assets/images/icons/delivery-van.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/server-islands/public/assets/images/icons/money-back.svg b/examples/server-islands/public/assets/images/icons/money-back.svg
new file mode 100644
index 000000000000..8722d553853c
--- /dev/null
+++ b/examples/server-islands/public/assets/images/icons/money-back.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/server-islands/public/assets/images/icons/office.svg b/examples/server-islands/public/assets/images/icons/office.svg
new file mode 100644
index 000000000000..27451293b5d7
--- /dev/null
+++ b/examples/server-islands/public/assets/images/icons/office.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/examples/server-islands/public/assets/images/icons/outdoor-cafe.svg b/examples/server-islands/public/assets/images/icons/outdoor-cafe.svg
new file mode 100644
index 000000000000..2f014997946d
--- /dev/null
+++ b/examples/server-islands/public/assets/images/icons/outdoor-cafe.svg
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/server-islands/public/assets/images/icons/phone.svg b/examples/server-islands/public/assets/images/icons/phone.svg
new file mode 100644
index 000000000000..1ab3dafd2160
--- /dev/null
+++ b/examples/server-islands/public/assets/images/icons/phone.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/server-islands/public/assets/images/icons/restaurant.svg b/examples/server-islands/public/assets/images/icons/restaurant.svg
new file mode 100644
index 000000000000..859ca4457609
--- /dev/null
+++ b/examples/server-islands/public/assets/images/icons/restaurant.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/server-islands/public/assets/images/icons/service-hours.svg b/examples/server-islands/public/assets/images/icons/service-hours.svg
new file mode 100644
index 000000000000..311820ec5c45
--- /dev/null
+++ b/examples/server-islands/public/assets/images/icons/service-hours.svg
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/server-islands/public/assets/images/icons/sofa.svg b/examples/server-islands/public/assets/images/icons/sofa.svg
new file mode 100644
index 000000000000..61608eb42e03
--- /dev/null
+++ b/examples/server-islands/public/assets/images/icons/sofa.svg
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/server-islands/public/assets/images/icons/terrace.svg b/examples/server-islands/public/assets/images/icons/terrace.svg
new file mode 100644
index 000000000000..40cd91ff4fc9
--- /dev/null
+++ b/examples/server-islands/public/assets/images/icons/terrace.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/server-islands/public/assets/images/logo.svg b/examples/server-islands/public/assets/images/logo.svg
new file mode 100644
index 000000000000..4a698caaad39
--- /dev/null
+++ b/examples/server-islands/public/assets/images/logo.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/examples/server-islands/public/assets/images/methods.png b/examples/server-islands/public/assets/images/methods.png
new file mode 100644
index 000000000000..6ab2499aa3cb
Binary files /dev/null and b/examples/server-islands/public/assets/images/methods.png differ
diff --git a/examples/server-islands/public/assets/images/offer.jpg b/examples/server-islands/public/assets/images/offer.jpg
new file mode 100644
index 000000000000..1b735cdb8801
Binary files /dev/null and b/examples/server-islands/public/assets/images/offer.jpg differ
diff --git a/examples/server-islands/public/assets/images/products/product1.jpg b/examples/server-islands/public/assets/images/products/product1.jpg
new file mode 100644
index 000000000000..523c18d1ce82
Binary files /dev/null and b/examples/server-islands/public/assets/images/products/product1.jpg differ
diff --git a/examples/server-islands/public/assets/images/products/product10.jpg b/examples/server-islands/public/assets/images/products/product10.jpg
new file mode 100644
index 000000000000..234c4b1bacf0
Binary files /dev/null and b/examples/server-islands/public/assets/images/products/product10.jpg differ
diff --git a/examples/server-islands/public/assets/images/products/product11.jpg b/examples/server-islands/public/assets/images/products/product11.jpg
new file mode 100644
index 000000000000..8f23255cf6fb
Binary files /dev/null and b/examples/server-islands/public/assets/images/products/product11.jpg differ
diff --git a/examples/server-islands/public/assets/images/products/product12.jpg b/examples/server-islands/public/assets/images/products/product12.jpg
new file mode 100644
index 000000000000..f1f32d6ddf4b
Binary files /dev/null and b/examples/server-islands/public/assets/images/products/product12.jpg differ
diff --git a/examples/server-islands/public/assets/images/products/product2.jpg b/examples/server-islands/public/assets/images/products/product2.jpg
new file mode 100644
index 000000000000..ab2fc5d8b59d
Binary files /dev/null and b/examples/server-islands/public/assets/images/products/product2.jpg differ
diff --git a/examples/server-islands/public/assets/images/products/product3.jpg b/examples/server-islands/public/assets/images/products/product3.jpg
new file mode 100644
index 000000000000..dd2e27bc4d5a
Binary files /dev/null and b/examples/server-islands/public/assets/images/products/product3.jpg differ
diff --git a/examples/server-islands/public/assets/images/products/product4.jpg b/examples/server-islands/public/assets/images/products/product4.jpg
new file mode 100644
index 000000000000..4e31e49b8008
Binary files /dev/null and b/examples/server-islands/public/assets/images/products/product4.jpg differ
diff --git a/examples/server-islands/public/assets/images/products/product5.jpg b/examples/server-islands/public/assets/images/products/product5.jpg
new file mode 100644
index 000000000000..e950c71fd260
Binary files /dev/null and b/examples/server-islands/public/assets/images/products/product5.jpg differ
diff --git a/examples/server-islands/public/assets/images/products/product6.jpg b/examples/server-islands/public/assets/images/products/product6.jpg
new file mode 100644
index 000000000000..fb68277067b4
Binary files /dev/null and b/examples/server-islands/public/assets/images/products/product6.jpg differ
diff --git a/examples/server-islands/public/assets/images/products/product7.jpg b/examples/server-islands/public/assets/images/products/product7.jpg
new file mode 100644
index 000000000000..e0c355a9bb3d
Binary files /dev/null and b/examples/server-islands/public/assets/images/products/product7.jpg differ
diff --git a/examples/server-islands/public/assets/images/products/product8.jpg b/examples/server-islands/public/assets/images/products/product8.jpg
new file mode 100644
index 000000000000..bca29d380cf1
Binary files /dev/null and b/examples/server-islands/public/assets/images/products/product8.jpg differ
diff --git a/examples/server-islands/public/assets/images/products/product9.jpg b/examples/server-islands/public/assets/images/products/product9.jpg
new file mode 100644
index 000000000000..7e4558371a70
Binary files /dev/null and b/examples/server-islands/public/assets/images/products/product9.jpg differ
diff --git a/examples/server-islands/src/base.css b/examples/server-islands/src/base.css
new file mode 100644
index 000000000000..90e97c7ff114
--- /dev/null
+++ b/examples/server-islands/src/base.css
@@ -0,0 +1,32 @@
+@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap");
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ body {
+ @apply font-poppins;
+ }
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ @apply font-roboto;
+ }
+}
+
+@layer components {
+ .size-selector input:checked + label {
+ @apply bg-primary text-white;
+ }
+ .color-selector input:checked + label {
+ @apply ring-2 ring-primary;
+ }
+
+ .input-box {
+ @apply block w-full border border-gray-300 px-4 py-3 text-gray-600 text-sm rounded placeholder-gray-400 focus:border-primary focus:ring-0;
+ }
+}
diff --git a/examples/server-islands/src/cart.ts b/examples/server-islands/src/cart.ts
new file mode 100644
index 000000000000..355074a38c3b
--- /dev/null
+++ b/examples/server-islands/src/cart.ts
@@ -0,0 +1,20 @@
+
+const channel = new MessageChannel();
+
+function onNewCartItem(cb: (m: any) => void) {
+ let onMessage = (ev: MessageEvent) => {
+ cb(ev.data);
+ };
+ channel.port2.addEventListener('message', onMessage);
+ channel.port2.start();
+ return () => channel.port2.removeEventListener('message', onMessage);
+}
+
+function addToCart(item: any) {
+ channel.port1.postMessage(item);
+}
+
+export {
+ onNewCartItem,
+ addToCart
+}
diff --git a/examples/server-islands/src/components/AddToCart.tsx b/examples/server-islands/src/components/AddToCart.tsx
new file mode 100644
index 000000000000..5fcfc4eaa172
--- /dev/null
+++ b/examples/server-islands/src/components/AddToCart.tsx
@@ -0,0 +1,27 @@
+import { addToCart } from '../cart';
+
+export default function({ small }) {
+ function onClick(ev: Event) {
+ ev.preventDefault();
+ let item = { name: 'Sofa' };
+ addToCart(item);
+
+ }
+
+ if(small) {
+ return (
+ Add
+ to cart
+ )
+ }
+
+ return (
+
+ Add to cart
+
+ )
+}
diff --git a/examples/server-islands/src/components/CartCount.tsx b/examples/server-islands/src/components/CartCount.tsx
new file mode 100644
index 000000000000..5c3d3e3928d3
--- /dev/null
+++ b/examples/server-islands/src/components/CartCount.tsx
@@ -0,0 +1,14 @@
+import { useEffect, useState } from 'react';
+import { onNewCartItem } from '../cart';
+
+export default function({ count: initialCount }) {
+ const [count, setCount] = useState(initialCount);
+ useEffect(() => {
+ return onNewCartItem(() => setCount(count + 1));
+ }, [count]);
+
+ return (
+
+{count}
+ );
+}
diff --git a/examples/server-islands/src/components/PersonalBar.astro b/examples/server-islands/src/components/PersonalBar.astro
new file mode 100644
index 000000000000..197ade129793
--- /dev/null
+++ b/examples/server-islands/src/components/PersonalBar.astro
@@ -0,0 +1,32 @@
+---
+import CartCount from './CartCount';
+
+const { placeholder } = Astro.props;
+let wishlist = 0;
+let cart = 0;
+
+if(!placeholder) {
+ await new Promise(resolve => setTimeout(resolve, 3000));
+}
+---
+
+
+
+
+ Wishlist
+ { wishlist }
+
+
+
+
+
+ Cart
+
+
+
+
+
+
+ Account
+
diff --git a/examples/server-islands/src/pages/index.astro b/examples/server-islands/src/pages/index.astro
new file mode 100644
index 000000000000..a36d5df05f4a
--- /dev/null
+++ b/examples/server-islands/src/pages/index.astro
@@ -0,0 +1,543 @@
+---
+import '../base.css';
+import AddToCart from '../components/AddToCart';
+import PersonalBar from '../components/PersonalBar.astro';
+import '@fortawesome/fontawesome-free/css/all.min.css';
+---
+
+
+
+
+
+
+
+ Product - Ecommerce Tailwind
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
All Categories
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Italian L Shape Sofa
+
+
+
+
+
+
+
+
+
(150 Reviews)
+
+
+
+ Availability:
+ In Stock
+
+
+ Brand:
+ Apex
+
+
+ Category:
+ Sofa
+
+
+ SKU:
+ BE45VGRT
+
+
+
+
+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Eos eius eum
+ reprehenderit dolore vel mollitia optio consequatur hic asperiores inventore suscipit, velit
+ consequuntur, voluptate doloremque iure necessitatibus adipisci magnam porro.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Product details
+
+
+
Lorem, ipsum dolor sit amet consectetur adipisicing elit. Tenetur necessitatibus deleniti natus
+ dolore cum maiores suscipit optio itaque voluptatibus veritatis tempora iste facilis non aut
+ sapiente dolor quisquam, ex ab.
+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, quae accusantium voluptatem
+ blanditiis sapiente voluptatum. Autem ab, dolorum assumenda earum veniam eius illo fugiat possimus
+ illum dolor totam, ducimus excepturi.
+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Error quia modi ut expedita! Iure molestiae
+ labore cumque nobis quasi fuga, quibusdam rem? Temporibus consectetur corrupti rerum veritatis
+ numquam labore amet.
+
+
+
+
+ Color
+ Blank, Brown, Red
+
+
+ Material
+ Latex
+
+
+ Weight
+ 55kg
+
+
+
+
+
+
+
+
+
Related products
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Lorem ipsum dolor sit amet consectetur adipisicing elit. Quia, hic?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
© TailCommerce - All Right Reserved
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/server-islands/tailwind.config.cjs b/examples/server-islands/tailwind.config.cjs
new file mode 100644
index 000000000000..4076218af7f3
--- /dev/null
+++ b/examples/server-islands/tailwind.config.cjs
@@ -0,0 +1,27 @@
+module.exports = {
+ content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
+ theme: {
+ screen: {
+ sm: "576px",
+ md: "768px",
+ lg: "992px",
+ xl: "1200px",
+ },
+ container: {
+ center: true,
+ padding: "1rem",
+ },
+ extend: {
+ fontFamily: {
+ poppins: ["Poppins", "sans-serif"],
+ roboto: ["Roboto", "sans-serif"],
+ },
+ colors: {
+ primary: "#fd3d57",
+ },
+ },
+ },
+ plugins: [
+ require("@tailwindcss/forms"),
+ ],
+};
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..f03c53335d30
--- /dev/null
+++ b/packages/astro/e2e/fixtures/server-islands/astro.config.mjs
@@ -0,0 +1,14 @@
+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()],
+ experimental: {
+ serverIslands: true,
+ }
+});
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..b7c376f517ad
--- /dev/null
+++ b/packages/astro/e2e/fixtures/server-islands/src/components/Island.astro
@@ -0,0 +1,4 @@
+---
+---
+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..71b7d0e266c8
--- /dev/null
+++ b/packages/astro/e2e/fixtures/server-islands/src/pages/index.astro
@@ -0,0 +1,14 @@
+---
+import Island from '../components/Island.astro';
+---
+
+
+
+
+
+
+
+ children
+
+
+
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..1479b807b121
--- /dev/null
+++ b/packages/astro/e2e/server-islands.test.js
@@ -0,0 +1,66 @@
+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('Slots are provided back to the server islands', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/'));
+ let el = page.locator('#children');
+
+ await expect(el, 'element rendered').toBeVisible();
+ });
+ });
+
+ 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/package.json b/packages/astro/package.json
index a83a148861cb..bd24ecaac792 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -125,7 +125,7 @@
"test:node": "astro-scripts test \"test/**/*.test.js\""
},
"dependencies": {
- "@astrojs/compiler": "^2.8.2",
+ "@astrojs/compiler": "^2.9.0",
"@astrojs/internal-helpers": "workspace:*",
"@astrojs/markdown-remark": "workspace:*",
"@astrojs/telemetry": "workspace:*",
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 81199ea94361..33cb5f0d0b13 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -17,7 +17,7 @@ import type {
ActionInputSchema,
} from '../actions/runtime/virtual/server.js';
import type { RemotePattern } from '../assets/utils/remotePattern.js';
-import type { AssetsPrefix, SerializedSSRManifest } from '../core/app/types.js';
+import type { AssetsPrefix, SSRManifest, SerializedSSRManifest } from '../core/app/types.js';
import type { PageBuildData } from '../core/build/types.js';
import type { AstroConfigType } from '../core/config/index.js';
import type { AstroTimer } from '../core/config/timer.js';
@@ -90,6 +90,7 @@ export interface AstroBuiltinProps {
'client:media'?: string;
'client:visible'?: ClientVisibleOptions | boolean;
'client:only'?: boolean | string;
+ 'server:defer'?: boolean;
}
export type ClientVisibleOptions = Pick;
@@ -2202,6 +2203,71 @@ export interface AstroUserConfig {
*/
validateSecrets?: boolean;
};
+
+ /**
+ * @docs
+ * @name experimental.serverIslands
+ * @type {boolean}
+ * @default `false`
+ * @version 4.12.0
+ * @description
+ *
+ * Enables experimental Server Island features.
+ * Server Islands offer the ability to defer a component to render asynchronously after the page has already rendered.
+ *
+ * To enable, configure an [on-demand server rendering `output` mode](https://docs.astro.build/en/basics/rendering-modes/#on-demand-rendered) with an adapter, and add the `serverIslands` flag to the `experimental` object:
+ *
+ * ```js
+ * {
+ * output: 'hybrid', // or 'server'
+ * adapter: nodejs({ mode: 'standalone' }),
+ * experimental: {
+ * serverIslands: true,
+ * },
+ * }
+ * ```
+ *
+ * Use the `server:defer` directive on any Astro component to delay initial rendering:
+ *
+ * ```astro "server:defer"
+ * ---
+ * import Avatar from '~/components/Avatar.astro';
+ * ---
+ *
+ * ```
+ *
+ * The outer page will be rendered, either at build-time (`hybrid`) or at runtime (`server`) with the island content omitted and a ``)
+ }
+ }
+}
diff --git a/packages/astro/src/runtime/server/render/slot.ts b/packages/astro/src/runtime/server/render/slot.ts
index 95f9008250aa..fae7edc9eb9b 100644
--- a/packages/astro/src/runtime/server/render/slot.ts
+++ b/packages/astro/src/runtime/server/render/slot.ts
@@ -1,8 +1,8 @@
import type { SSRResult } from '../../../@types/astro.js';
-import type { renderTemplate } from './astro/render-template.js';
+import { renderTemplate } from './astro/render-template.js';
import type { RenderInstruction } from './instruction.js';
-import { HTMLString, markHTMLString } from '../escape.js';
+import { HTMLString, markHTMLString, unescapeHTML } from '../escape.js';
import { renderChild } from './any.js';
import { type RenderDestination, type RenderInstance, chunkToString } from './common.js';
@@ -103,3 +103,9 @@ export async function renderSlots(
}
return { slotInstructions, children };
}
+
+export function createSlotValueFromString(content: string): ComponentSlotValue {
+ return function() {
+ return renderTemplate`${unescapeHTML(content)}`;
+ };
+}
diff --git a/packages/astro/src/vite-plugin-astro-server/pipeline.ts b/packages/astro/src/vite-plugin-astro-server/pipeline.ts
index 4ad3c48c8ba6..c41d5bbcd973 100644
--- a/packages/astro/src/vite-plugin-astro-server/pipeline.ts
+++ b/packages/astro/src/vite-plugin-astro-server/pipeline.ts
@@ -12,13 +12,13 @@ import type {
} from '../@types/astro.js';
import { getInfoOutput } from '../cli/info/index.js';
import { type HeadElements } from '../core/base-pipeline.js';
-import { ASTRO_VERSION, DEFAULT_404_COMPONENT } from '../core/constants.js';
+import { ASTRO_VERSION } from '../core/constants.js';
import { enhanceViteSSRError } from '../core/errors/dev/index.js';
import { AggregateError, CSSError, MarkdownError } from '../core/errors/index.js';
import type { Logger } from '../core/logger/core.js';
import type { ModuleLoader } from '../core/module-loader/index.js';
import { Pipeline, loadRenderer } from '../core/render/index.js';
-import { default404Page } from '../core/routing/astro-designed-error-pages.js';
+import { createDefaultRoutes } from '../core/routing/default.js';
import { findRouteToRewrite } from '../core/routing/rewrite.js';
import { isPage, isServerLikeOutput, viteID } from '../core/util.js';
import { resolveIdToUrl } from '../core/viteUtils.js';
@@ -45,13 +45,16 @@ export class DevPipeline extends Pipeline {
readonly logger: Logger,
readonly manifest: SSRManifest,
readonly settings: AstroSettings,
- readonly config = settings.config
+ readonly config = settings.config,
+ readonly defaultRoutes = createDefaultRoutes(manifest, config.root)
) {
const mode = 'development';
const resolve = createResolve(loader, config.root);
const serverLike = isServerLikeOutput(config);
const streaming = true;
super(logger, manifest, mode, [], resolve, serverLike, streaming);
+ manifest.serverIslandMap = settings.serverIslandMap;
+ manifest.serverIslandNameMap = settings.serverIslandNameMap;
}
static create(
@@ -153,8 +156,12 @@ export class DevPipeline extends Pipeline {
async preload(routeData: RouteData, filePath: URL) {
const { loader } = this;
- if (filePath.href === new URL(DEFAULT_404_COMPONENT, this.config.root).href) {
- return { default: default404Page } as any as ComponentInstance;
+
+ // First check built-in routes
+ for(const route of this.defaultRoutes) {
+ if(route.matchesComponent(filePath)) {
+ return route.instance;
+ }
}
// Important: This needs to happen first, in case a renderer provides polyfills.
@@ -216,4 +223,16 @@ export class DevPipeline extends Pipeline {
setManifestData(manifestData: ManifestData) {
this.manifestData = manifestData;
}
+
+ rewriteKnownRoute(route: string, sourceRoute: RouteData): ComponentInstance {
+ if (isServerLikeOutput(this.config) && sourceRoute.prerender) {
+ for(let def of this.defaultRoutes) {
+ if(route === def.route) {
+ return def.instance;
+ }
+ }
+ }
+
+ throw new Error('Unknown route');
+ }
}
diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts
index 9df0502ea320..ba94d7cbf27c 100644
--- a/packages/astro/src/vite-plugin-astro-server/plugin.ts
+++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts
@@ -9,8 +9,8 @@ import { AstroError, AstroErrorData } from '../core/errors/index.js';
import { patchOverlay } from '../core/errors/overlay.js';
import type { Logger } from '../core/logger/core.js';
import { createViteLoader } from '../core/module-loader/index.js';
-import { ensure404Route } from '../core/routing/astro-designed-error-pages.js';
import { createRouteManifest } from '../core/routing/index.js';
+import { injectDefaultRoutes } from '../core/routing/default.js';
import { toRoutingStrategy } from '../i18n/utils.js';
import { baseMiddleware } from './base.js';
import { createController } from './controller.js';
@@ -35,8 +35,8 @@ export default function createVitePluginAstroServer({
configureServer(viteServer) {
const loader = createViteLoader(viteServer);
const manifest = createDevelopmentManifest(settings);
- let manifestData: ManifestData = ensure404Route(
- createRouteManifest({ settings, fsMod }, logger)
+ let manifestData: ManifestData = injectDefaultRoutes(
+ createRouteManifest({ settings, fsMod }, logger),
);
const pipeline = DevPipeline.create(manifestData, { loader, logger, manifest, settings });
const controller = createController({ loader });
@@ -46,7 +46,9 @@ export default function createVitePluginAstroServer({
function rebuildManifest(needsManifestRebuild: boolean) {
pipeline.clearRouteCache();
if (needsManifestRebuild) {
- manifestData = ensure404Route(createRouteManifest({ settings }, logger));
+ manifestData = injectDefaultRoutes(
+ createRouteManifest({ settings }, logger),
+ );
pipeline.setManifestData(manifestData);
}
}
diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts
index 36449ea24359..9f087b621c15 100644
--- a/packages/astro/src/vite-plugin-astro-server/route.ts
+++ b/packages/astro/src/vite-plugin-astro-server/route.ts
@@ -12,7 +12,6 @@ import { loadMiddleware } from '../core/middleware/loadMiddleware.js';
import { RenderContext } from '../core/render-context.js';
import { type SSROptions, getProps } from '../core/render/index.js';
import { createRequest } from '../core/request.js';
-import { default404Page } from '../core/routing/astro-designed-error-pages.js';
import { matchAllRoutes } from '../core/routing/index.js';
import { getSortedPreloadedMatches } from '../prerender/routing.js';
import type { DevPipeline } from './pipeline.js';
@@ -106,19 +105,6 @@ export async function matchRoute(
const custom404 = getCustom404Route(manifestData);
- if (custom404 && custom404.component === DEFAULT_404_COMPONENT) {
- const component: ComponentInstance = {
- default: default404Page,
- };
- return {
- route: custom404,
- filePath: new URL(`file://${custom404.component}`),
- resolvedPathname: pathname,
- preloadedComponent: component,
- mod: component,
- };
- }
-
if (custom404) {
const filePath = new URL(`./${custom404.component}`, config.root);
const preloadedComponent = await pipeline.preload(custom404, filePath);
diff --git a/packages/astro/src/vite-plugin-astro/index.ts b/packages/astro/src/vite-plugin-astro/index.ts
index cedf49e95bce..456b7677de63 100644
--- a/packages/astro/src/vite-plugin-astro/index.ts
+++ b/packages/astro/src/vite-plugin-astro/index.ts
@@ -214,6 +214,7 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl
const astroMetadata: AstroPluginMetadata['astro'] = {
clientOnlyComponents: transformResult.clientOnlyComponents,
hydratedComponents: transformResult.hydratedComponents,
+ serverComponents: transformResult.serverComponents,
scripts: transformResult.scripts,
containsHead: transformResult.containsHead,
propagation: transformResult.propagation ? 'self' : 'none',
diff --git a/packages/astro/src/vite-plugin-astro/metadata.ts b/packages/astro/src/vite-plugin-astro/metadata.ts
index d0a2b3644b22..3fa0068a5a40 100644
--- a/packages/astro/src/vite-plugin-astro/metadata.ts
+++ b/packages/astro/src/vite-plugin-astro/metadata.ts
@@ -7,3 +7,15 @@ export function getAstroMetadata(modInfo: ModuleInfo): PluginMetadata['astro'] |
}
return undefined;
}
+
+export function createDefaultAstroMetadata(): PluginMetadata['astro'] {
+ return {
+ hydratedComponents: [],
+ clientOnlyComponents: [],
+ serverComponents: [],
+ scripts: [],
+ propagation: 'none',
+ containsHead: false,
+ pageOptions: {},
+ };
+}
diff --git a/packages/astro/src/vite-plugin-astro/types.ts b/packages/astro/src/vite-plugin-astro/types.ts
index 8e82165f5ebd..ee5003941526 100644
--- a/packages/astro/src/vite-plugin-astro/types.ts
+++ b/packages/astro/src/vite-plugin-astro/types.ts
@@ -10,6 +10,7 @@ export interface PluginMetadata {
astro: {
hydratedComponents: TransformResult['hydratedComponents'];
clientOnlyComponents: TransformResult['clientOnlyComponents'];
+ serverComponents: TransformResult['serverComponents'];
scripts: TransformResult['scripts'];
containsHead: TransformResult['containsHead'];
propagation: PropagationHint;
diff --git a/packages/astro/src/vite-plugin-markdown/index.ts b/packages/astro/src/vite-plugin-markdown/index.ts
index 98362c89d443..25a5ab64844c 100644
--- a/packages/astro/src/vite-plugin-markdown/index.ts
+++ b/packages/astro/src/vite-plugin-markdown/index.ts
@@ -13,9 +13,9 @@ import { AstroError, AstroErrorData } from '../core/errors/index.js';
import type { Logger } from '../core/logger/core.js';
import { isMarkdownFile } from '../core/util.js';
import { shorthash } from '../runtime/server/shorthash.js';
-import type { PluginMetadata } from '../vite-plugin-astro/types.js';
import { getFileInfo } from '../vite-plugin-utils/index.js';
import { type MarkdownImagePath, getMarkdownCodeForImages } from './images.js';
+import { createDefaultAstroMetadata } from '../vite-plugin-astro/metadata.js';
interface AstroPluginOptions {
settings: AstroSettings;
@@ -159,14 +159,7 @@ export default function markdown({ settings, logger }: AstroPluginOptions): Plug
return {
code,
meta: {
- astro: {
- hydratedComponents: [],
- clientOnlyComponents: [],
- scripts: [],
- propagation: 'none',
- containsHead: false,
- pageOptions: {},
- } as PluginMetadata['astro'],
+ astro: createDefaultAstroMetadata(),
vite: {
lang: 'ts',
},
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..70d0e6d6aadb
--- /dev/null
+++ b/packages/astro/test/fixtures/server-islands/hybrid/astro.config.mjs
@@ -0,0 +1,13 @@
+import svelte from '@astrojs/svelte';
+import { defineConfig } from 'astro/config';
+
+export default defineConfig({
+ output: 'hybrid',
+ integrations: [
+ svelte()
+ ],
+ experimental: {
+ serverIslands: true,
+ }
+});
+
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..8eb474b04853
--- /dev/null
+++ b/packages/astro/test/fixtures/server-islands/ssr/astro.config.mjs
@@ -0,0 +1,13 @@
+import svelte from '@astrojs/svelte';
+import { defineConfig } from 'astro/config';
+
+export default defineConfig({
+ output: 'server',
+ integrations: [
+ svelte()
+ ],
+ experimental: {
+ serverIslands: true,
+ }
+});
+
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 4d527f0ffea9..f16d08abf03b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -373,6 +373,45 @@ importers:
specifier: ^4.11.6
version: link:../../packages/astro
+ examples/server-islands:
+ devDependencies:
+ '@astrojs/node':
+ specifier: ^8.2.6
+ version: link:../../packages/integrations/node
+ '@astrojs/react':
+ specifier: ^3.6.0
+ version: link:../../packages/integrations/react
+ '@astrojs/tailwind':
+ specifier: ^5.1.0
+ version: link:../../packages/integrations/tailwind
+ '@fortawesome/fontawesome-free':
+ specifier: ^6.5.2
+ version: 6.5.2
+ '@tailwindcss/forms':
+ specifier: ^0.5.7
+ version: 0.5.7(tailwindcss@3.4.5)
+ '@types/react':
+ specifier: ^18.3.3
+ version: 18.3.3
+ '@types/react-dom':
+ specifier: ^18.3.0
+ version: 18.3.0
+ astro:
+ specifier: workspace:*
+ version: link:../../packages/astro
+ postcss:
+ specifier: ^8.4.38
+ version: 8.4.39
+ react:
+ specifier: ^18.3.1
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.3.1
+ version: 18.3.1(react@18.3.1)
+ tailwindcss:
+ specifier: ^3.4.3
+ version: 3.4.5
+
examples/ssr:
dependencies:
'@astrojs/node':
@@ -529,8 +568,8 @@ importers:
packages/astro:
dependencies:
'@astrojs/compiler':
- specifier: ^2.8.2
- version: 2.8.2
+ specifier: ^2.9.0
+ version: 2.9.1
'@astrojs/internal-helpers':
specifier: workspace:*
version: link:../internal-helpers
@@ -1554,6 +1593,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':
@@ -3592,6 +3652,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:
@@ -5968,8 +6052,8 @@ packages:
'@astrojs/compiler@1.8.2':
resolution: {integrity: sha512-o/ObKgtMzl8SlpIdzaxFnt7SATKPxu4oIP/1NL+HDJRzxfJcAkOTAb/ZKMRyULbz4q+1t2/DAebs2Z1QairkZw==}
- '@astrojs/compiler@2.8.2':
- resolution: {integrity: sha512-2v2N2oDnMH6+CX1Wn6f45Afa4tdkUMutdx8pJaokfaOYnAU+u6+UK7o7sXqydKro1cLwVmmOIJv6AqiXnAdLDA==}
+ '@astrojs/compiler@2.9.1':
+ resolution: {integrity: sha512-s8Ge2lWHx/s3kl4UoerjL/iPtwdtogNM/BLOaGCwQA6crMOVYpphy5wUkYlKyuh8GAeGYH/5haLAFBsgNy9AQQ==}
'@astrojs/language-server@2.11.1':
resolution: {integrity: sha512-WSIBBUK9lSeVD4KhPiZk2u3wsXdj7WEYvYPPs8ZsgbSVIOzUJWAKVcITHiXmcXlzZB5ubK44YUN/Hq+f2GeMyQ==}
@@ -6744,6 +6828,10 @@ packages:
'@fontsource/montserrat@5.0.18':
resolution: {integrity: sha512-85JBs2rCdFK/VBdSb401e2lXk5gynVo2zi3Rh2Guem4WNtT2q52+V90o3KzjmajY3TPJvCZA/kI7R05ev7148g==}
+ '@fortawesome/fontawesome-free@6.5.2':
+ resolution: {integrity: sha512-hRILoInAx8GNT5IMkrtIt9blOdrqHOnPBH+k70aWUAqPZPgopb9G5EQJFpaBx/S8zp2fC+mPW349Bziuk1o28Q==}
+ engines: {node: '>=6'}
+
'@humanwhocodes/module-importer@1.0.1':
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
engines: {node: '>=12.22'}
@@ -7239,6 +7327,11 @@ packages:
svelte: ^4.0.0 || ^5.0.0-next.0
vite: ^5.0.0
+ '@tailwindcss/forms@0.5.7':
+ resolution: {integrity: sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==}
+ peerDependencies:
+ tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1'
+
'@trysound/sax@0.2.0':
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
@@ -9687,6 +9780,10 @@ packages:
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
engines: {node: '>=12'}
+ mini-svg-data-uri@1.4.4:
+ resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
+ hasBin: true
+
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -11759,11 +11856,11 @@ snapshots:
'@astrojs/compiler@1.8.2': {}
- '@astrojs/compiler@2.8.2': {}
+ '@astrojs/compiler@2.9.1': {}
'@astrojs/language-server@2.11.1(prettier-plugin-astro@0.14.0)(prettier@3.3.3)(typescript@5.5.3)':
dependencies:
- '@astrojs/compiler': 2.8.2
+ '@astrojs/compiler': 2.9.1
'@jridgewell/sourcemap-codec': 1.4.15
'@volar/kit': 2.4.0-alpha.16(typescript@5.5.3)
'@volar/language-core': 2.4.0-alpha.16
@@ -12645,6 +12742,8 @@ snapshots:
'@fontsource/montserrat@5.0.18': {}
+ '@fortawesome/fontawesome-free@6.5.2': {}
+
'@humanwhocodes/module-importer@1.0.1': {}
'@humanwhocodes/retry@0.3.0': {}
@@ -13156,6 +13255,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@tailwindcss/forms@0.5.7(tailwindcss@3.4.5)':
+ dependencies:
+ mini-svg-data-uri: 1.4.4
+ tailwindcss: 3.4.5
+
'@trysound/sax@0.2.0': {}
'@ts-morph/common@0.20.0':
@@ -16148,6 +16252,8 @@ snapshots:
mimic-fn@4.0.0: {}
+ mini-svg-data-uri@1.4.4: {}
+
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.11