diff --git a/.changeset/dry-worms-knock.md b/.changeset/dry-worms-knock.md new file mode 100644 index 0000000000..e8412e3be8 --- /dev/null +++ b/.changeset/dry-worms-knock.md @@ -0,0 +1,18 @@ +--- +'astro': major +--- + +The `image.endpoint` config now allow customizing the route of the image endpoint in addition to the entrypoint. This can be useful in niche situations where the default route `/_image` conflicts with an existing route or your local server setup. + +```js +import { defineConfig } from "astro/config"; + +defineConfig({ + image: { + endpoint: { + route: "/image", + entrypoint: "./src/image_endpoint.ts" + } + }, +}) +``` diff --git a/packages/astro/src/assets/endpoint/config.ts b/packages/astro/src/assets/endpoint/config.ts index 382645d6ee..a910df25cc 100644 --- a/packages/astro/src/assets/endpoint/config.ts +++ b/packages/astro/src/assets/endpoint/config.ts @@ -1,4 +1,9 @@ +import { + removeLeadingForwardSlash, + removeTrailingForwardSlash, +} from '@astrojs/internal-helpers/path'; import { resolveInjectedRoute } from '../../core/routing/manifest/create.js'; +import { getPattern } from '../../core/routing/manifest/pattern.js'; import type { AstroSettings, ManifestData } from '../../types/astro.js'; import type { RouteData } from '../../types/public/internal.js'; @@ -28,19 +33,34 @@ function getImageEndpointData( cwd?: string, ): RouteData { const endpointEntrypoint = - settings.config.image.endpoint ?? - (mode === 'dev' ? 'astro/assets/endpoint/node' : 'astro/assets/endpoint/generic'); + settings.config.image.endpoint.entrypoint === undefined // If not set, use default endpoint + ? mode === 'dev' + ? 'astro/assets/endpoint/node' + : 'astro/assets/endpoint/generic' + : settings.config.image.endpoint.entrypoint; + + const segments = [ + [ + { + content: removeTrailingForwardSlash( + removeLeadingForwardSlash(settings.config.image.endpoint.route), + ), + dynamic: false, + spread: false, + }, + ], + ]; return { type: 'endpoint', isIndex: false, - route: '/_image', - pattern: /^\/_image$/, - segments: [[{ content: '_image', dynamic: false, spread: false }]], + route: settings.config.image.endpoint.route, + pattern: getPattern(segments, settings.config.base, settings.config.trailingSlash), + segments, params: [], component: resolveInjectedRoute(endpointEntrypoint, settings.config.root, cwd).component, generate: () => '', - pathname: '/_image', + pathname: settings.config.image.endpoint.route, prerender: false, fallbackRoutes: [], }; diff --git a/packages/astro/src/assets/services/service.ts b/packages/astro/src/assets/services/service.ts index f798e1dfd1..e22bada898 100644 --- a/packages/astro/src/assets/services/service.ts +++ b/packages/astro/src/assets/services/service.ts @@ -33,7 +33,7 @@ interface SharedServiceProps = Record /** * Return the URL to the endpoint or URL your images are generated from. * - * For a local service, your service should expose an endpoint handling the image requests, or use Astro's at `/_image`. + * For a local service, your service should expose an endpoint handling the image requests, or use Astro's which by default, is located at `/_image`. * * For external services, this should point to the URL your images are coming from, for instance, `/_vercel/image` * @@ -343,7 +343,7 @@ export const baseService: Omit = { options[key] && searchParams.append(param, options[key].toString()); }); - const imageEndpoint = joinPaths(import.meta.env.BASE_URL, '/_image'); + const imageEndpoint = joinPaths(import.meta.env.BASE_URL, imageConfig.endpoint.route); return `${imageEndpoint}?${searchParams}`; }, parseURL(url) { diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts index 5c40d98b4f..7c60a34a2d 100644 --- a/packages/astro/src/assets/vite-plugin-assets.ts +++ b/packages/astro/src/assets/vite-plugin-assets.ts @@ -101,7 +101,7 @@ export default function assets({ }; return [ - // Expose the components and different utilities from `astro:assets` and handle serving images from `/_image` in dev + // Expose the components and different utilities from `astro:assets` { name: 'astro:assets', async resolveId(id) { diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index d59e9966e9..9eaf4ff1d6 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -412,7 +412,6 @@ async function generatePath( ); const request = createRequest({ - base: config.base, url, headers: new Headers(), logger, diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts index 60427a19e8..924c10f8fa 100644 --- a/packages/astro/src/core/config/schema.ts +++ b/packages/astro/src/core/config/schema.ts @@ -65,6 +65,7 @@ export const ASTRO_CONFIG_DEFAULTS = { inlineStylesheets: 'auto', }, image: { + endpoint: { entrypoint: undefined, route: '/_image' }, service: { entrypoint: 'astro/assets/services/sharp', config: {} }, }, devToolbar: { @@ -240,7 +241,15 @@ export const AstroConfigSchema = z.object({ .optional(), image: z .object({ - endpoint: z.string().optional(), + endpoint: z + .object({ + route: z + .literal('/_image') + .or(z.string()) + .default(ASTRO_CONFIG_DEFAULTS.image.endpoint.route), + entrypoint: z.string().optional(), + }) + .default(ASTRO_CONFIG_DEFAULTS.image.endpoint), service: z .object({ entrypoint: z diff --git a/packages/astro/src/core/request.ts b/packages/astro/src/core/request.ts index 78d2979b5f..3ace80ba8f 100644 --- a/packages/astro/src/core/request.ts +++ b/packages/astro/src/core/request.ts @@ -5,7 +5,6 @@ type HeaderType = Headers | Record | IncomingHttpHeaders; type RequestBody = ArrayBuffer | Blob | ReadableStream | URLSearchParams | FormData; export interface CreateRequestOptions { - base: string; url: URL | string; clientAddress?: string | undefined; headers: HeaderType; diff --git a/packages/astro/src/types/public/config.ts b/packages/astro/src/types/public/config.ts index 365b3a75d6..7a712eb5c1 100644 --- a/packages/astro/src/types/public/config.ts +++ b/packages/astro/src/types/public/config.ts @@ -904,24 +904,28 @@ export interface AstroUserConfig { /** * @docs * @name image.endpoint - * @type {string} - * @default `undefined` + * @type {{route: string, entrypoint: undefined | string}} + * @default `{route: '/_image', entrypoint: undefined}` * @version 3.1.0 * @description - * Set the endpoint to use for image optimization in dev and SSR. Set to `undefined` to use the default endpoint. - * - * The endpoint will always be injected at `/_image`. + * Set the endpoint to use for image optimization in dev and SSR. The `entrypoint` property can be set to `undefined` to use the default image endpoint. * * ```js * { * image: { - * // Example: Use a custom image endpoint - * endpoint: './src/image-endpoint.ts', + * // Example: Use a custom image endpoint at `/custom_endpoint` + * endpoint: { + * route: '/custom_endpoint', + * entrypoint: 'src/my_endpoint.ts', + * }, * }, * } * ``` */ - endpoint?: string; + endpoint?: { + route: '/_image' | (string & {}); + entrypoint: undefined | string; + }; /** * @docs diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index 2bf5a31e49..8df09b8984 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -166,9 +166,9 @@ export async function handleRoute({ const filePath: URL | undefined = matchedRoute.filePath; const { preloadedComponent } = matchedRoute; route = matchedRoute.route; + // Allows adapters to pass in locals in dev mode. request = createRequest({ - base: config.base, url, headers: incomingRequest.headers, method: incomingRequest.method, diff --git a/packages/astro/test/core-image.test.js b/packages/astro/test/core-image.test.js index 303bcc7f0d..c3490a5034 100644 --- a/packages/astro/test/core-image.test.js +++ b/packages/astro/test/core-image.test.js @@ -646,7 +646,7 @@ describe('astro:image', () => { customEndpointFixture = await loadFixture({ root: './fixtures/core-image/', image: { - endpoint: './src/custom-endpoint.ts', + endpoint: { entrypoint: './src/custom-endpoint.ts' }, service: testImageService({ foo: 'bar' }), domains: ['avatars.githubusercontent.com'], }, @@ -1133,7 +1133,7 @@ describe('astro:image', () => { outDir: './dist/server-prod', adapter: testAdapter(), image: { - endpoint: 'astro/assets/endpoint/node', + endpoint: { entrypoint: 'astro/assets/endpoint/node' }, service: testImageService(), }, });