Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sour-taxis-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': minor
---

Added config option for setting a hostname to be prepended to Server Island URLs. This will allow users to deploy Server Island server-side code seperately from generated static output.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a minor, a new feature, so we should show more love. Here's some tips on how to do that: https://contribute.docs.astro.build/docs-for-code-changes/changesets/#new-features

3 changes: 2 additions & 1 deletion packages/astro/src/core/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ export type SSRManifest = {
pageModule?: SinglePageBuiltModule;
pageMap?: Map<ComponentPath, ImportComponentInstance>;
serverIslandMappings?: () => Promise<ServerIslandMappings> | ServerIslandMappings;
key: Promise<CryptoKey>;
serverIslandHostname?: AstroConfig['serverIslandHostname'];
key: Promise<CryptoKey>;
i18n: SSRManifestI18n | undefined;
middleware?: () => Promise<AstroMiddlewareInstance> | AstroMiddlewareInstance;
logger?: () =>
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ async function buildManifest(
middlewareMode,
site: settings.config.site,
base: settings.config.base,
serverIslandHostname: settings.config.serverIslandHostname,
userAssetsBase: settings.config?.vite?.base,
trailingSlash: settings.config.trailingSlash,
compressHTML: settings.config.compressHTML,
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/core/config/schemas/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const ASTRO_CONFIG_DEFAULTS = {
outDir: './dist',
cacheDir: './node_modules/.astro',
base: '/',
serverIslandHostname: '',
trailingSlash: 'ignore',
build: {
format: 'directory',
Expand Down Expand Up @@ -560,6 +561,7 @@ export const AstroConfigSchema = z.object({
})
.optional(),
fonts: z.array(FontFamilySchema).optional(),
serverIslandHostname: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.serverIslandHostname),
experimental: z
.strictObject({
clientPrerender: z
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/fetch/fetch-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ export class FetchState implements AstroFetchState {
},
key: manifest.key,
trailingSlash: manifest.trailingSlash,
serverIslandHostname: manifest.serverIslandHostname,
_metadata: {
hasHydrationScript: false,
rendererSpecificHydrationScripts: new Set(),
Expand Down
3 changes: 3 additions & 0 deletions packages/astro/src/core/routing/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface RouterOptions {
base: AstroConfig['base'];
trailingSlash: AstroConfig['trailingSlash'];
buildFormat: NonNullable<AstroConfig['build']>['format'];
serverIslandHostname?: AstroConfig['serverIslandHostname'];
}

interface RouterMatchRoute {
Expand Down Expand Up @@ -51,6 +52,7 @@ export class Router {
#baseWithoutTrailingSlash: string;
#buildFormat: RouterOptions['buildFormat'];
#trailingSlash: RouterOptions['trailingSlash'];
#serverIslandHostname: RouterOptions['serverIslandHostname'];

constructor(routes: RouteData[], options: RouterOptions) {
// Copy before sorting to avoid mutating the caller's route list.
Expand All @@ -60,6 +62,7 @@ export class Router {
this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#base);
this.#buildFormat = options.buildFormat;
this.#trailingSlash = options.trailingSlash;
this.#serverIslandHostname = options.serverIslandHostname;
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/manifest/serialized.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ async function createSerializedManifest(
clientDirectives: Array.from(settings.clientDirectives.entries()),
renderers: [],
base: settings.config.base,
serverIslandHostname: settings.config.serverIslandHostname,
userAssetsBase: settings.config?.vite?.base,
assetsPrefix: settings.config.build.assetsPrefix,
site: settings.config.site,
Expand Down
5 changes: 3 additions & 2 deletions packages/astro/src/runtime/server/render/server-islands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,9 @@ export class ServerIslandComponent {
: await encryptString(key, JSON.stringify(renderedSlots), `slots:${componentId}`);

const hostId = await this.getHostId();
const slash = this.result.base.endsWith('/') ? '' : '/';
let serverIslandUrl = `${this.result.base}${slash}_server-islands/${componentId}${this.result.trailingSlash === 'always' ? '/' : ''}`;
const serverIslandHostname = this.result.serverIslandHostname?.replace(/\/$/, '') || '';
const base = this.result.base === '/' ? '' : this.result.base.replace(/\/$/, '');
let serverIslandUrl = `${serverIslandHostname}${base}/_server-islands/${componentId}${this.result.trailingSlash === 'always' ? '/' : ''}`;
Comment thread
ryechus marked this conversation as resolved.
Outdated

// Determine if its safe to use a GET request
const potentialSearchParams = createSearchParams(
Expand Down
24 changes: 24 additions & 0 deletions packages/astro/src/types/public/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,30 @@ export interface AstroUserConfig<
*/
base?: string;

/**
* @docs
* @name serverIslandHostname
* @type {string}
* @description
* Sets a hostname or origin prefix to use when Astro fetches server island content.
* This value is prepended to the server island request URL, which is useful when
* you want those requests to go to a different origin (for example, a CDN or
* dedicated API domain) than the page itself.
*
* The value should include the protocol and, if needed, a port. Astro will
* normalize the URL so the server island route is appended correctly.
*
* ```js
* {
* serverIslandHostname: 'https://cdn.example.com'
* }
* ```
*
* With a base path of `/docs`, a server island request will be fetched from:
* `https://cdn.example.com/docs/_server-islands/...`
*/
serverIslandHostname?: string;

/**
* @docs
* @name trailingSlash
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/types/public/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ export interface SSRResult {
cookies: AstroCookies | undefined;
getServerIslandNameMap: () => Promise<Map<string, string>>;
trailingSlash: AstroConfig['trailingSlash'];
serverIslandHostname?: AstroConfig['serverIslandHostname'];
key: Promise<CryptoKey>;
_metadata: SSRMetadata;
/**
Expand Down
10 changes: 10 additions & 0 deletions packages/astro/test/units/manifest/serialized.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ describe('serializedManifestPlugin - dev mode', () => {
});
});

describe('serverIslandHostname', () => {
it('preserves the configured hostname', async () => {
const settings = await createBasicSettings({
serverIslandHostname: 'https://example.com',
});
const manifest = await getManifest(settings);
assert.equal(manifest.serverIslandHostname, 'https://example.com');
});
});

describe('serverLike', () => {
it('is true when buildOutput is server', async () => {
const settings = await createBasicSettings({});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,50 @@ describe('ServerIslandComponent', () => {
);
});

it('uses the configured serverIslandHostname as a URL prefix', async () => {
const result = await createStubResult({
base: '/docs',
serverIslandHostname: 'https://cdn.example.com',
});
const component = new ServerIslandComponent(result, islandProps(), {}, 'Island');
const content = await component.getIslandContent();
assert.ok(
content.includes('https://cdn.example.com/docs/_server-islands/Island'),
`server island URL should be prefixed by serverIslandHostname, got: ${content}`,
);
});

it('normalizes a trailing slash in serverIslandHostname', async () => {
const result = await createStubResult({
base: '/docs',
serverIslandHostname: 'https://cdn.example.com/',
});
const component = new ServerIslandComponent(result, islandProps(), {}, 'Island');
const content = await component.getIslandContent();
assert.ok(
content.includes('https://cdn.example.com/docs/_server-islands/Island'),
`server island URL should not contain a double slash, got: ${content}`,
);
assert.ok(
!content.includes('https://cdn.example.com//docs/_server-islands/Island'),
'server island URL should not double-escape the base path',
);
});

it('keeps the relative URL when serverIslandHostname is unset', async () => {
const result = await createStubResult({ serverIslandHostname: undefined });
const component = new ServerIslandComponent(result, islandProps(), {}, 'Island');
const content = await component.getIslandContent();
assert.ok(
content.includes('/_server-islands/Island'),
`server island URL should remain relative when unset, got: ${content}`,
);
assert.ok(
!content.includes('https://') && !content.includes('http://'),
'server island URL should not add an absolute origin when unset',
);
});

it('appends a trailing slash when trailingSlash is "always"', async () => {
const result = await createStubResult({ trailingSlash: 'always' });
const component = new ServerIslandComponent(result, islandProps(), {}, 'Island');
Expand Down
Loading