diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index c3b687d01..40bd123d5 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -107,6 +107,7 @@ export class App { }, routeCache: new RouteCache(this.#logging), site: this.#manifest.site, + base: this.#manifest.base, ssr: true, streaming, }); diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index f8d1bf6b4..102f1ff1c 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -526,6 +526,7 @@ async function generatePath( }, routeCache, site: manifest.site, + base: manifest.base, ssr, streaming: true, }); diff --git a/packages/astro/src/core/endpoint/index.ts b/packages/astro/src/core/endpoint/index.ts index 9298e7cbe..d450cc48a 100644 --- a/packages/astro/src/core/endpoint/index.ts +++ b/packages/astro/src/core/endpoint/index.ts @@ -13,6 +13,7 @@ import { AstroCookies, attachCookiesToResponse } from '../cookies/index.js'; import { AstroError, AstroErrorData } from '../errors/index.js'; import { warn } from '../logger/core.js'; import { callMiddleware } from '../middleware/callMiddleware.js'; +import { createRouteUrl } from '../routing/url.js'; const clientAddressSymbol = Symbol.for('astro.clientAddress'); const clientLocalsSymbol = Symbol.for('astro.locals'); @@ -31,6 +32,7 @@ type CreateAPIContext = { request: Request; params: Params; site?: string; + routeUrl: URL; props: Record; adapterName?: string; }; @@ -44,6 +46,7 @@ export function createAPIContext({ request, params, site, + routeUrl, props, adapterName, }: CreateAPIContext): APIContext { @@ -62,7 +65,7 @@ export function createAPIContext({ }, }); }, - url: new URL(request.url), + url: routeUrl, get clientAddress() { if (!(clientAddressSymbol in request)) { if (adapterName) { @@ -102,11 +105,18 @@ export async function callEndpoint ctx: RenderContext, onRequest?: MiddlewareHandler | undefined ): Promise { + const routeUrl = createRouteUrl(ctx.route, { + params: ctx.params, + base: env.base, + site: env.site ?? new URL(ctx.request.url).origin, + }); + const context = createAPIContext({ request: ctx.request, params: ctx.params, props: ctx.props, site: env.site, + routeUrl, adapterName: env.adapterName, }); diff --git a/packages/astro/src/core/middleware/index.ts b/packages/astro/src/core/middleware/index.ts index 90c5bdb5e..467c4b5e1 100644 --- a/packages/astro/src/core/middleware/index.ts +++ b/packages/astro/src/core/middleware/index.ts @@ -29,6 +29,7 @@ function createContext({ request, params }: CreateContext) { params: params ?? {}, props: {}, site: undefined, + routeUrl: new URL(request.url) }); } diff --git a/packages/astro/src/core/pipeline.ts b/packages/astro/src/core/pipeline.ts index 66fa6bd07..a1a7a58c7 100644 --- a/packages/astro/src/core/pipeline.ts +++ b/packages/astro/src/core/pipeline.ts @@ -10,6 +10,7 @@ import type { } from '../@types/astro'; import { callMiddleware } from './middleware/callMiddleware.js'; import { renderPage } from './render/core.js'; +import { createRouteUrl } from './routing/url.js'; type EndpointResultHandler = ( originalRequest: Request, @@ -95,11 +96,18 @@ export class Pipeline { mod: Readonly, onRequest?: MiddlewareHandler ): Promise { + const routeUrl = createRouteUrl(renderContext.route, { + params: renderContext.params, + base: env.base, + site: env.site ?? new URL(renderContext.request.url).origin, + }); + const apiContext = createAPIContext({ request: renderContext.request, params: renderContext.params, props: renderContext.props, site: env.site, + routeUrl, adapterName: env.adapterName, }); diff --git a/packages/astro/src/core/render/core.ts b/packages/astro/src/core/render/core.ts index 9de046278..44d6c28b8 100644 --- a/packages/astro/src/core/render/core.ts +++ b/packages/astro/src/core/render/core.ts @@ -11,6 +11,7 @@ import { attachCookiesToResponse } from '../cookies/index.js'; import { callEndpoint, createAPIContext, type EndpointCallResult } from '../endpoint/index.js'; import { callMiddleware } from '../middleware/callMiddleware.js'; import { redirectRouteGenerate, redirectRouteStatus, routeIsRedirect } from '../redirects/index.js'; +import { createRouteUrl } from '../routing/url.js'; import type { RenderContext } from './context.js'; import type { Environment } from './environment.js'; import { createResult } from './result.js'; @@ -37,11 +38,18 @@ export async function renderPage({ mod, renderContext, env, cookies }: RenderPag if (!Component) throw new Error(`Expected an exported Astro component but received typeof ${typeof Component}`); + const routeUrl = createRouteUrl(renderContext.route, { + params: renderContext.params, + base: env.base, + site: env.site ?? new URL(renderContext.request.url).origin, + }); + const result = createResult({ adapterName: env.adapterName, links: renderContext.links, styles: renderContext.styles, logging: env.logging, + routeUrl, params: renderContext.params, pathname: renderContext.pathname, componentMetadata: renderContext.componentMetadata, @@ -93,11 +101,18 @@ export async function tryRenderRoute( mod: Readonly, onRequest?: MiddlewareHandler ): Promise { + const routeUrl = createRouteUrl(renderContext.route, { + params: renderContext.params, + base: env.base, + site: env.site ?? new URL(renderContext.request.url).origin, + }); + const apiContext = createAPIContext({ request: renderContext.request, params: renderContext.params, props: renderContext.props, site: env.site, + routeUrl, adapterName: env.adapterName, }); diff --git a/packages/astro/src/core/render/environment.ts b/packages/astro/src/core/render/environment.ts index 32dfb454b..3b65e7ea7 100644 --- a/packages/astro/src/core/render/environment.ts +++ b/packages/astro/src/core/render/environment.ts @@ -26,6 +26,10 @@ export interface Environment { * Used for `Astro.site` */ site?: string; + /** + * Used to derive `Astro.url` + */ + base?: string; /** * Value of Astro config's `output` option, true if "server" or "hybrid" */ diff --git a/packages/astro/src/core/render/result.ts b/packages/astro/src/core/render/result.ts index 72fa4ddcf..987d5c280 100644 --- a/packages/astro/src/core/render/result.ts +++ b/packages/astro/src/core/render/result.ts @@ -36,6 +36,10 @@ export interface CreateResultArgs { * Used for `Astro.site` */ site: string | undefined; + /** + * Used for `Astro.url`. A stable route usually derived from `RouteData`. + */ + routeUrl: URL; links?: Set; scripts?: Set; styles?: Set; @@ -126,7 +130,6 @@ class Slots { export function createResult(args: CreateResultArgs): SSRResult { const { params, request, resolve, locals } = args; - const url = new URL(request.url); const headers = new Headers(); headers.set('Content-Type', 'text/html'); const response: ResponseInit = { @@ -195,7 +198,7 @@ export function createResult(args: CreateResultArgs): SSRResult { props, locals, request, - url, + url: args.routeUrl, redirect(path, status) { // If the response is already sent, error as we cannot proceed with the redirect. if ((request as any)[responseSentSymbol]) { diff --git a/packages/astro/src/core/routing/index.ts b/packages/astro/src/core/routing/index.ts index b568bb121..22419cd25 100644 --- a/packages/astro/src/core/routing/index.ts +++ b/packages/astro/src/core/routing/index.ts @@ -2,4 +2,5 @@ export { createRouteManifest } from './manifest/create.js'; export { deserializeRouteData, serializeRouteData } from './manifest/serialization.js'; export { matchAllRoutes, matchRoute } from './match.js'; export { getParams } from './params.js'; +export { createRouteUrl } from './url.js'; export { validateDynamicRouteModule, validateGetStaticPathsResult } from './validation.js'; diff --git a/packages/astro/src/core/routing/url.ts b/packages/astro/src/core/routing/url.ts new file mode 100644 index 000000000..32b3a0f59 --- /dev/null +++ b/packages/astro/src/core/routing/url.ts @@ -0,0 +1,24 @@ +import type { Params, RouteData } from '../../@types/astro.js'; +import { joinPaths } from '../path.js'; + +interface CreateUrlOptions { + params?: Params; + site?: string; + base?: string; +} + +export function createRouteUrl(route: RouteData, options: CreateUrlOptions) { + const site = options.site ?? 'http://localhost:4321'; + const base = options.base ?? '/'; + + // Tests don't implement generate, do a dirty skip here + if (route.generate == null) { + return new URL(base, site); + } + + const pathnameWithoutBase = route.generate(options.params); + // If the pathname is empty (root without trailing slash), return it as is so the final + // URL also doesn't have a trailing slash + const pathname = pathnameWithoutBase === '' ? '' : joinPaths(base, pathnameWithoutBase); + return new URL(pathname, site); +} diff --git a/packages/astro/src/vite-plugin-astro-server/environment.ts b/packages/astro/src/vite-plugin-astro-server/environment.ts index ce7b92662..3f68e05b3 100644 --- a/packages/astro/src/vite-plugin-astro-server/environment.ts +++ b/packages/astro/src/vite-plugin-astro-server/environment.ts @@ -25,6 +25,7 @@ export function createDevelopmentEnvironment( resolve: createResolve(loader, settings.config.root), routeCache: new RouteCache(logging, mode), site: manifest.site, + base: manifest.base, ssr: isServerLikeOutput(settings.config), streaming: true, }); diff --git a/packages/astro/test/astro-get-static-paths.test.js b/packages/astro/test/astro-get-static-paths.test.js index 66aa5b94d..be13dd34f 100644 --- a/packages/astro/test/astro-get-static-paths.test.js +++ b/packages/astro/test/astro-get-static-paths.test.js @@ -29,7 +29,7 @@ describe('getStaticPaths - build calls', () => { const html = await fixture.readFile('/food/tacos/index.html'); const $ = cheerio.load(html); - expect($('#url').text()).to.equal('/blog/food/tacos/'); + expect($('#url').text()).to.equal('/blog/food/tacos'); }); }); diff --git a/packages/astro/test/astro-global.test.js b/packages/astro/test/astro-global.test.js index f003bc035..7959ea0d4 100644 --- a/packages/astro/test/astro-global.test.js +++ b/packages/astro/test/astro-global.test.js @@ -24,16 +24,16 @@ describe('Astro Global', () => { await devServer.stop(); }); - it('Astro.request.url', async () => { + it('Astro.url', async () => { const res = await await fixture.fetch('/blog/?foo=42'); expect(res.status).to.equal(200); const html = await res.text(); const $ = cheerio.load(html); - expect($('#pathname').text()).to.equal('/blog/'); + expect($('#pathname').text()).to.equal('/blog'); expect($('#searchparams').text()).to.equal('{}'); - expect($('#child-pathname').text()).to.equal('/blog/'); - expect($('#nested-child-pathname').text()).to.equal('/blog/'); + expect($('#child-pathname').text()).to.equal('/blog'); + expect($('#nested-child-pathname').text()).to.equal('/blog'); }); it('Astro.glob() returned `url` metadata of each markdown file extensions DOES NOT include the extension', async () => { diff --git a/packages/astro/test/ssr-prerender-get-static-paths.test.js b/packages/astro/test/ssr-prerender-get-static-paths.test.js index 3fe2950cb..45c71c86c 100644 --- a/packages/astro/test/ssr-prerender-get-static-paths.test.js +++ b/packages/astro/test/ssr-prerender-get-static-paths.test.js @@ -38,7 +38,7 @@ describe('Prerender', () => { const $ = cheerio.load(html); expect($('#props').text()).to.equal('10'); - expect($('#url').text()).to.equal('/blog/food/tacos/'); + expect($('#url').text()).to.equal('/blog/food/tacos'); }); }); @@ -169,7 +169,7 @@ describe('Prerender', () => { const $ = cheerio.load(html); expect($('#props').text()).to.equal('10'); - expect($('#url').text()).to.equal('/blog/food/tacos/'); + expect($('#url').text()).to.equal('/blog/food/tacos'); }); }); diff --git a/packages/astro/test/ssr-request.test.js b/packages/astro/test/ssr-request.test.js index 7bdce20b5..01fe1043a 100644 --- a/packages/astro/test/ssr-request.test.js +++ b/packages/astro/test/ssr-request.test.js @@ -50,8 +50,8 @@ describe('Using Astro.request in SSR', () => { const html = await response.text(); const $ = cheerioLoad(html); expect($('#origin').text()).to.equal('http://example.com'); - expect($('#pathname').text()).to.equal('/subpath/request/'); - expect($('#request-pathname').text()).to.equal('/subpath/request/'); + expect($('#pathname').text()).to.equal('/subpath/request'); + expect($('#request-pathname').text()).to.equal('/subpath/request'); }); it('public file is copied over', async () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a45afb8b2..538817c57 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18263,25 +18263,21 @@ packages: file:packages/astro/test/fixtures/css-assets/packages/font-awesome: resolution: {directory: packages/astro/test/fixtures/css-assets/packages/font-awesome, type: directory} name: '@test/astro-font-awesome-package' - version: 0.0.1 dev: false file:packages/astro/test/fixtures/multiple-renderers/renderers/one: resolution: {directory: packages/astro/test/fixtures/multiple-renderers/renderers/one, type: directory} name: '@test/astro-renderer-one' - version: 1.0.0 dev: false file:packages/astro/test/fixtures/multiple-renderers/renderers/two: resolution: {directory: packages/astro/test/fixtures/multiple-renderers/renderers/two, type: directory} name: '@test/astro-renderer-two' - version: 1.0.0 dev: false file:packages/astro/test/fixtures/solid-component/deps/solid-jsx-component: resolution: {directory: packages/astro/test/fixtures/solid-component/deps/solid-jsx-component, type: directory} name: '@test/solid-jsx-component' - version: 0.0.0 dependencies: solid-js: 1.7.6 dev: false