diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 7fbe7fa4ce40..ec799011907b 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -6,7 +6,7 @@ import type { SSRElement, SSRManifest, } from '../../@types/astro.js'; -import { createI18nMiddleware } from '../../i18n/middleware.js'; +import { createI18nMiddleware, i18nPipelineHook } from '../../i18n/middleware.js'; import type { SinglePageBuiltModule } from '../build/types.js'; import { getSetCookiesFromResponse } from '../cookies/index.js'; import { consoleLogDestination } from '../logger/console.js'; @@ -169,8 +169,7 @@ export class App { let i18nMiddleware = createI18nMiddleware( this.#manifest.i18n, this.#manifest.base, - this.#manifest.trailingSlash, - this.#manifest.routes.map((route) => route.routeData) + this.#manifest.trailingSlash ); if (i18nMiddleware) { if (mod.onRequest) { @@ -180,6 +179,7 @@ export class App { } else { this.#pipeline.setMiddlewareFunction(i18nMiddleware); } + this.#pipeline.onBeforeRenderRoute(i18nPipelineHook); } else { if (mod.onRequest) { this.#pipeline.setMiddlewareFunction(mod.onRequest as MiddlewareEndpointHandler); diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 961d78ed4865..02837cf69cc8 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -30,7 +30,7 @@ import { removeLeadingForwardSlash, removeTrailingForwardSlash, } from '../../core/path.js'; -import { createI18nMiddleware } from '../../i18n/middleware.js'; +import { createI18nMiddleware, i18nPipelineHook } from '../../i18n/middleware.js'; import { runHookBuildGenerated } from '../../integrations/index.js'; import { getOutputDirectory, isServerLikeOutput } from '../../prerender/utils.js'; import { PAGE_SCRIPT_ID } from '../../vite-plugin-scripts/index.js'; @@ -279,8 +279,7 @@ async function generatePage( const i18nMiddleware = createI18nMiddleware( pipeline.getManifest().i18n, pipeline.getManifest().base, - pipeline.getManifest().trailingSlash, - pipeline.getManifest().routes.map((route) => route.routeData) + pipeline.getManifest().trailingSlash ); if (config.experimental.i18n && i18nMiddleware) { if (onRequest) { @@ -290,6 +289,7 @@ async function generatePage( } else { pipeline.setMiddlewareFunction(i18nMiddleware); } + pipeline.onBeforeRenderRoute(i18nPipelineHook); } else if (onRequest) { pipeline.setMiddlewareFunction(onRequest as MiddlewareEndpointHandler); } diff --git a/packages/astro/src/core/pipeline.ts b/packages/astro/src/core/pipeline.ts index 3d33146c6fa9..bd203b437415 100644 --- a/packages/astro/src/core/pipeline.ts +++ b/packages/astro/src/core/pipeline.ts @@ -15,6 +15,12 @@ type EndpointResultHandler = ( result: Response ) => Promise | Response; +type PipelineHooks = { + before: PipelineHookFunction[]; +}; + +export type PipelineHookFunction = (ctx: RenderContext, mod: ComponentInstance | undefined) => void; + /** * This is the basic class of a pipeline. * @@ -23,6 +29,9 @@ type EndpointResultHandler = ( export class Pipeline { env: Environment; #onRequest?: MiddlewareEndpointHandler; + #hooks: PipelineHooks = { + before: [], + }; /** * The handler accepts the *original* `Request` and result returned by the endpoint. * It must return a `Response`. @@ -75,6 +84,9 @@ export class Pipeline { renderContext: RenderContext, componentInstance: ComponentInstance | undefined ): Promise { + for (const hook of this.#hooks.before) { + hook(renderContext, componentInstance); + } const result = await this.#tryRenderRoute( renderContext, this.env, @@ -158,4 +170,12 @@ export class Pipeline { throw new Error(`Couldn't find route of type [${renderContext.route.type}]`); } } + + /** + * Store a function that will be called before starting the rendering phase. + * @param fn + */ + onBeforeRenderRoute(fn: PipelineHookFunction) { + this.#hooks.before.push(fn); + } } diff --git a/packages/astro/src/i18n/middleware.ts b/packages/astro/src/i18n/middleware.ts index 017f062ba040..ff467ecb3131 100644 --- a/packages/astro/src/i18n/middleware.ts +++ b/packages/astro/src/i18n/middleware.ts @@ -1,6 +1,9 @@ import { appendForwardSlash, joinPaths } from '@astrojs/internal-helpers/path'; import type { MiddlewareEndpointHandler, RouteData, SSRManifest } from '../@types/astro.js'; import type { RouteInfo } from '../core/app/types.js'; +import type { PipelineHookFunction } from '../core/pipeline.js'; + +const routeDataSymbol = Symbol.for('astro.routeData'); // Checks if the pathname doesn't have any locale, exception for the defaultLocale, which is ignored on purpose function checkIsLocaleFree(pathname: string, locales: string[]): boolean { @@ -16,34 +19,30 @@ function checkIsLocaleFree(pathname: string, locales: string[]): boolean { export function createI18nMiddleware( i18n: SSRManifest['i18n'], base: SSRManifest['base'], - trailingSlash: SSRManifest['trailingSlash'], - routes: RouteData[] + trailingSlash: SSRManifest['trailingSlash'] ): MiddlewareEndpointHandler | undefined { if (!i18n) { return undefined; } - // we get all the routes that aren't pages - const nonPagesRoutes = routes.filter((route) => { - return route.type !== 'page'; - }); - return async (context, next) => { if (!i18n) { return await next(); } - const url = context.url; - // We get the pathname - // Internally, Astro removes the `base` from the manifest data of the routes. - // We have to make sure that we remove it from the pathname of the request - let astroPathname = url.pathname; - if (astroPathname.startsWith(base) && base !== '/') { - astroPathname = astroPathname.slice(base.length); + const routeData = Reflect.get(context.request, routeDataSymbol); + if (routeData) { + // If the route we're processing is not a page, then we ignore it + if ( + (routeData as RouteData).type !== 'page' && + (routeData as RouteData).type !== 'fallback' + ) { + return await next(); + } } + const url = context.url; const { locales, defaultLocale, fallback } = i18n; - const response = await next(); if (response instanceof Response) { @@ -58,11 +57,6 @@ export function createI18nMiddleware( headers: response.headers, }); } else if (i18n.routingStrategy === 'prefix-always') { - // We want to do this check only here, because `prefix-other-locales` assumes that non localized folder are valid - if (shouldSkipRoute(astroPathname, nonPagesRoutes, locales)) { - return await next(); - } - if (url.pathname === base + '/' || url.pathname === base) { if (trailingSlash === 'always') { return context.redirect(`${appendForwardSlash(joinPaths(base, i18n.defaultLocale))}`); @@ -105,21 +99,8 @@ export function createI18nMiddleware( } /** - * Checks whether a route should be skipped from the middleware logic. A route should be not be skipped when: - * - it's the home - * - it contains any locale - * - the pathname belongs to a route that is not a page + * This pipeline hook attaches a `RouteData` object to the `Request` */ -function shouldSkipRoute(pathname: string, pageRoutes: RouteData[], locales: string[]) { - if (!pathname.length || pathname === '/') { - return false; - } - - if (locales.some((locale) => pathname.includes(`/${locale}`))) { - return false; - } - - return pageRoutes.some((route) => { - return !route.pattern.test(pathname); - }); -} +export const i18nPipelineHook: PipelineHookFunction = (ctx) => { + Reflect.set(ctx.request, routeDataSymbol, ctx.route); +}; diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index 616015563272..7468f881907a 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -20,7 +20,7 @@ import { import { createRequest } from '../core/request.js'; import { matchAllRoutes } from '../core/routing/index.js'; import { isPage, resolveIdToUrl } from '../core/util.js'; -import { createI18nMiddleware } from '../i18n/middleware.js'; +import { createI18nMiddleware, i18nPipelineHook } from '../i18n/middleware.js'; import { getSortedPreloadedMatches } from '../prerender/routing.js'; import { isServerLikeOutput } from '../prerender/utils.js'; import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js'; @@ -280,8 +280,7 @@ export async function handleRoute({ const i18Middleware = createI18nMiddleware( config.experimental.i18n, config.base, - config.trailingSlash, - manifestData.routes + config.trailingSlash ); if (i18Middleware) { @@ -290,6 +289,7 @@ export async function handleRoute({ } else { pipeline.setMiddlewareFunction(i18Middleware); } + pipeline.onBeforeRenderRoute(i18nPipelineHook); } else if (onRequest) { pipeline.setMiddlewareFunction(onRequest); }