Skip to content
Open
Show file tree
Hide file tree
Changes from all 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>;
apiPrefix?: AstroConfig['build']['apiPrefix'];
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,
apiPrefix: settings.config.build.apiPrefix,
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 @@ -75,6 +75,7 @@ export const ASTRO_CONFIG_DEFAULTS = {
redirects: true,
inlineStylesheets: 'auto',
concurrency: 1,
apiPrefix: '',
},
image: {
endpoint: { entrypoint: undefined, route: '/_image' },
Expand Down Expand Up @@ -225,6 +226,7 @@ export const AstroConfigSchema = z.object({
.optional()
.or(z.object({ fallback: z.string() }).and(z.record(z.string(), z.string())))
.optional(),
apiPrefix: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.apiPrefix),
serverEntry: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.serverEntry),
redirects: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.build.redirects),
inlineStylesheets: z
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/config/schemas/relative.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: string) {
.string()
.optional()
.or(z.object({ fallback: z.string() }).and(z.record(z.string(), z.string())).optional()),
apiPrefix: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.apiPrefix),
serverEntry: z.string().optional().default(ASTRO_CONFIG_DEFAULTS.build.serverEntry),
redirects: z.boolean().optional().default(ASTRO_CONFIG_DEFAULTS.build.redirects),
inlineStylesheets: 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,
apiPrefix: manifest.apiPrefix,
_metadata: {
hasHydrationScript: false,
rendererSpecificHydrationScripts: new Set(),
Expand Down
1 change: 1 addition & 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'];
apiPrefix?: AstroConfig['build']['apiPrefix'];
}

interface RouterMatchRoute {
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,
apiPrefix: settings.config.build.apiPrefix,
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 apiPrefix = this.result.apiPrefix?.replace(/\/$/, '') || '';
const base = this.result.base === '/' ? '' : this.result.base.replace(/\/$/, '');
let serverIslandUrl = `${apiPrefix}${base}/_server-islands/${componentId}${this.result.trailingSlash === 'always' ? '/' : ''}`;

// Determine if its safe to use a GET request
const potentialSearchParams = createSearchParams(
Expand Down
25 changes: 25 additions & 0 deletions packages/astro/src/types/public/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1200,6 +1200,31 @@ export interface AstroUserConfig<
*
*/
assetsPrefix?: AssetsPrefix;
/**
* @docs
* @name apiPrefix
* @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
* {
* build: {
* apiPrefix: '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/...`
*/
apiPrefix?: string;
/**
* @docs
* @name build.serverEntry
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'];
apiPrefix?: AstroConfig['build']['apiPrefix'];
key: Promise<CryptoKey>;
_metadata: SSRMetadata;
/**
Expand Down
12 changes: 12 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,18 @@ describe('serializedManifestPlugin - dev mode', () => {
});
});

describe('apiPrefix', () => {
it('preserves the configured hostname', async () => {
const settings = await createBasicSettings({
build: {
apiPrefix: 'https://example.com'
},
});
const manifest = await getManifest(settings);
assert.equal(manifest.apiPrefix, '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 apiPrefix as a URL prefix', async () => {
const result = await createStubResult({
base: '/docs',
apiPrefix: '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 apiPrefix, got: ${content}`,
);
});

it('normalizes a trailing slash in apiPrefix', async () => {
const result = await createStubResult({
base: '/docs',
apiPrefix: '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 apiPrefix is unset', async () => {
const result = await createStubResult({ apiPrefix: 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