Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .changeset/compress-html-jsx-default.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'astro': major
---

Makes `'jsx'` the default value for `compressHTML`

Astro now strips whitespace from your HTML using JSX rules by default, the same way frameworks like React do. Whitespace and line breaks around elements are removed, but meaningful whitespace within a single line — like a space between two inline elements — is preserved. To keep a space that would otherwise be removed, write it explicitly in your source, for example with `{" "}`.

This can change rendered output where whitespace between inline elements was previously meaningful. To keep Astro's earlier behavior, set `compressHTML: true` for HTML-aware compression, or `compressHTML: false` to preserve all whitespace.
21 changes: 21 additions & 0 deletions .changeset/container-renderer-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@astrojs/react': minor
'@astrojs/preact': minor
'@astrojs/svelte': minor
'@astrojs/solid-js': minor
'@astrojs/vue': minor
'@astrojs/mdx': minor
---

Replaces the import entrypoint of `getContainerRenderer()`

A new `container-renderer` entrypoint exporting `getContainerRenderer()` has been added to the following integrations: React, Preact, Svelte, SolidJS, Vue, and MDX. This prevents bundlers from trying to bundle unrelated exports from the package root when only the Container API is used.

If you are using the Container API, update your import statements to use the new entrypoint. The following example updates the `getContainerRenderer()` import for React:

```diff
- import { getContainerRenderer } from '@astrojs/react';
+ import { getContainerRenderer } from '@astrojs/react/container-renderer';
```

Importing `getContainerRenderer()` from the package root still works, but is now deprecated and logs a warning.
5 changes: 5 additions & 0 deletions .changeset/fifty-ways-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Improves build performance by removing an unfiltered transform hook from the `astro:head-metadata-build` plugin. Head propagation modules are now identified by their module ID (`?astroPropagatedAssets`) instead of scanning every module's source code.
5 changes: 5 additions & 0 deletions .changeset/fix-middleware-500-error-prop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes the custom `500.astro` page receiving an empty `error` prop when the error originated in middleware.
5 changes: 5 additions & 0 deletions .changeset/ripe-bottles-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes internal Astro headers leaking from direct `pages()` handler responses
2 changes: 1 addition & 1 deletion .flue/workflows/fix-verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const agent = createAgent(() => ({
GH_TOKEN: GITHUB_TOKEN_BASE,
},
}),
model: 'anthropic/claude-sonnet-4-20250801',
model: 'anthropic/claude-sonnet-4-6',
}));

export async function run({ init, payload }: FlueContext) {
Expand Down
9 changes: 7 additions & 2 deletions .flue/workflows/issue-triage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,12 +367,17 @@ export async function run({ init, payload }: FlueContext) {
),
});

await postGitHubComment(issueNumber, comment);
// The LLM sometimes returns literal "\n" (two characters) instead of actual
// newlines when producing a string via a tool-call result. Unescape them so
// the GitHub comment renders with real line breaks.
const normalizedComment = comment.replace(/\\n/g, '\n');

await postGitHubComment(issueNumber, normalizedComment);

if (triageResult.reproducible) {
await removeGitHubLabel(issueNumber, 'needs triage');
const selectedLabels = await selectTriageLabels(session, {
comment,
comment: normalizedComment,
priorityLabels,
packageLabels,
});
Expand Down
2 changes: 1 addition & 1 deletion examples/container-with-vitest/test/ReactWrapper.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { loadRenderers } from 'astro:container';
import { getContainerRenderer } from '@astrojs/react';
import { getContainerRenderer } from '@astrojs/react/container-renderer';
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { expect, test } from 'vitest';
import ReactWrapper from '../src/components/ReactWrapper.astro';
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@
"test:integration": "astro-scripts test \"test/*.test.ts\" --parallel --strip-types"
},
"dependencies": {
"@astrojs/compiler-rs": "^0.1.10",
"@astrojs/compiler-rs": "^0.2.2",
"@astrojs/internal-helpers": "workspace:*",
"@astrojs/markdown-satteri": "workspace:*",
"@astrojs/telemetry": "workspace:*",
Expand Down
13 changes: 3 additions & 10 deletions packages/astro/src/core/app/prepare-response.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { INTERNAL_RESPONSE_HEADERS, responseSentSymbol } from '../constants.js';
import { responseSentSymbol } from '../constants.js';
import { getSetCookiesFromResponse } from '../cookies/index.js';

/**
* Strips internal-only headers from the response before sending it to the
* user agent, and optionally appends cookies written via `Astro.cookie.set()`
* to the `Set-Cookie` header.
* Appends cookies written via `Astro.cookie.set()` to the `Set-Cookie` header
* and marks the response as sent.
*
* This is a pure function with no dependencies on the app; it is shared by
* `AstroHandler` and the various error handlers.
Expand All @@ -13,12 +12,6 @@ export function prepareResponse(
response: Response,
{ addCookieHeader }: { addCookieHeader: boolean },
): void {
for (const headerName of INTERNAL_RESPONSE_HEADERS) {
if (response.headers.has(headerName)) {
response.headers.delete(headerName);
}
}

if (addCookieHeader) {
for (const setCookieHeaderValue of getSetCookiesFromResponse(response)) {
response.headers.append('set-cookie', setCookieHeaderValue);
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ export async function renderPath({
relativeLocation: locationSite,
from: fromPath,
});
if (config.compressHTML === true) {
if (config.compressHTML) {
body = body.replaceAll('\n', '');
}
if (route.type !== 'redirect') {
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/core/config/schemas/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export const ASTRO_CONFIG_DEFAULTS = {
devToolbar: {
enabled: true,
},
compressHTML: true,
compressHTML: 'jsx',
server: {
host: false,
port: 4321,
Expand Down
52 changes: 0 additions & 52 deletions packages/astro/src/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,6 @@ export const ASTRO_VERSION = process.env.PACKAGE_VERSION ?? 'development';

export const ASTRO_GENERATOR = `Astro v${ASTRO_VERSION}`;

/**
* The name for the header used to help rerouting behavior.
* When set to "no", astro will NOT try to reroute an error response to the corresponding error page, which is the default behavior that can sometimes lead to loops.
*
* ```ts
* const response = new Response("keep this content as-is", {
* status: 404,
* headers: {
* // note that using a variable name as the key of an object needs to be wrapped in square brackets in javascript
* // without them, the header name will be interpreted as "REROUTE_DIRECTIVE_HEADER" instead of "X-Astro-Reroute"
* [REROUTE_DIRECTIVE_HEADER]: 'no',
* }
* })
* ```
* Alternatively...
* ```ts
* response.headers.set(REROUTE_DIRECTIVE_HEADER, 'no');
* ```
*/
export const REROUTE_DIRECTIVE_HEADER = 'X-Astro-Reroute';

/**
* Header and value that are attached to a Response object when a **user rewrite** occurs.
*
* This metadata is used to determine the origin of a Response. If a rewrite has occurred, it should be prioritised over other logic.
*/
export const REWRITE_DIRECTIVE_HEADER_KEY = 'X-Astro-Rewrite';

export const REWRITE_DIRECTIVE_HEADER_VALUE = 'yes';

/**
* This header is set by the no-op Astro middleware.
*/
export const NOOP_MIDDLEWARE_HEADER = 'X-Astro-Noop';

/**
* The name for the header used to help i18n middleware, which only needs to act on "page" and "fallback" route types.
*/
export const ROUTE_TYPE_HEADER = 'X-Astro-Route-Type';

/**
* Internal headers that should be stripped from the response before
* sending it to the user agent. Add new internal headers here so
* `prepareResponse` removes them automatically.
*/
export const INTERNAL_RESPONSE_HEADERS = [
REROUTE_DIRECTIVE_HEADER,
REWRITE_DIRECTIVE_HEADER_KEY,
NOOP_MIDDLEWARE_HEADER,
ROUTE_TYPE_HEADER,
] as const;

/**
* Set by internal handlers (e.g. PagesHandler) to signal that a
* response should be replaced with the corresponding error page.
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/core/errors/default-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export class DefaultErrorHandler implements ErrorHandler {
return this.renderError(request, {
...resolvedRenderOptions,
status,
error,
response: originalResponse,
skipMiddleware: true,
pathname: resolvedPathname,
Expand Down
9 changes: 9 additions & 0 deletions packages/astro/src/core/fetch/fetch-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ export class FetchState implements AstroFetchState {
clientAddress: string | undefined;
/** Whether this is a partial render (container API). */
partial: boolean | undefined;
/** Internal metadata about the current response route type. */
responseRouteType: 'page' | 'fallback' | undefined;
/** Internal flag to prevent rerouting this response to an error page. */
skipErrorReroute = false;
/** Whether to inject CSP meta tags. */
shouldInjectCspMetaTags: boolean | undefined;
/** Request-scoped locals object, shared with user middleware. */
Expand Down Expand Up @@ -1128,4 +1132,9 @@ export class FetchState implements AstroFetchState {
this.actionApiContext = null;
this.apiContext = null;
}

resetResponseMetadata(): void {
this.responseRouteType = undefined;
this.skipErrorReroute = false;
}
}
10 changes: 0 additions & 10 deletions packages/astro/src/core/head-propagation/hint.ts

This file was deleted.

21 changes: 7 additions & 14 deletions packages/astro/src/core/i18n/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { I18nRouter, type I18nRouterContext } from '../../i18n/router.js';
import { PipelineFeatures } from '../base-pipeline.js';
import type { SSRManifest } from '../app/types.js';
import { shouldAppendForwardSlash } from '../build/util.js';
import { REROUTE_DIRECTIVE_HEADER, ROUTE_TYPE_HEADER } from '../constants.js';
import type { FetchState } from '../fetch/fetch-state.js';

/**
Expand Down Expand Up @@ -59,34 +58,28 @@ export class I18n {
async finalize(state: FetchState, response: Response): Promise<Response> {
state.pipeline.usedFeatures |= PipelineFeatures.i18n;
const i18n = this.#i18n;
const typeHeader = response.headers.get(ROUTE_TYPE_HEADER);
// We don't need this header anymore, and we shouldn't pass downstream to users.
if (typeHeader) {
response.headers.delete(ROUTE_TYPE_HEADER);
}

// This is a case where we are internally rendering a 404/500, so we
// need to bypass checks that were done already.
const isReroute = response.headers.get(REROUTE_DIRECTIVE_HEADER);
if (isReroute === 'no' && typeof i18n.fallback === 'undefined') {
if (state.skipErrorReroute && typeof i18n.fallback === 'undefined') {
return response;
}

// If the route we're processing is not a page, then we ignore it
if (typeHeader !== 'page' && typeHeader !== 'fallback') {
if (state.responseRouteType !== 'page' && state.responseRouteType !== 'fallback') {
return response;
}

const url = new URL(state.request.url);
const currentLocale = state.computeCurrentLocale();
const isPrerendered = state.routeData!.prerender;

// Build context for router (typeHeader is guaranteed to be 'page' | 'fallback' here)
// Build context for router (responseRouteType is guaranteed to be 'page' | 'fallback' here)
const routerContext: I18nRouterContext = {
currentLocale,
currentDomain: url.hostname,
routeType: typeHeader as 'page' | 'fallback',
isReroute: isReroute === 'yes',
routeType: state.responseRouteType,
isReroute: false,
};

// Step 1: Apply routing strategy
Expand All @@ -113,7 +106,7 @@ export class I18n {
status: 404,
headers: response.headers,
});
prerenderedRes.headers.set(REROUTE_DIRECTIVE_HEADER, 'no');
state.skipErrorReroute = true;
if (routeDecision.location) {
prerenderedRes.headers.set('Location', routeDecision.location);
}
Expand All @@ -137,7 +130,7 @@ export class I18n {
// The fallback sentinel (X-Astro-Route-Type: fallback, status 500) signals
// that the render pipeline couldn't find this page in the current locale.
// Treat it as a 404 so computeFallbackRoute will apply fallback logic.
const effectiveStatus = typeHeader === 'fallback' ? 404 : response.status;
const effectiveStatus = state.responseRouteType === 'fallback' ? 404 : response.status;
const fallbackDecision = computeFallbackRoute({
pathname: url.pathname,
responseStatus: effectiveStatus,
Expand Down
2 changes: 0 additions & 2 deletions packages/astro/src/core/middleware/noop-middleware.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import type { MiddlewareHandler } from '../../types/public/common.js';
import { NOOP_MIDDLEWARE_HEADER } from '../constants.js';

export const NOOP_MIDDLEWARE_FN: MiddlewareHandler = async (_ctx, next) => {
const response = await next();
response.headers.set(NOOP_MIDDLEWARE_HEADER, 'true');
return response;
};
20 changes: 7 additions & 13 deletions packages/astro/src/core/pages/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,7 @@ import type { APIContext } from '../../types/public/context.js';
import type { BaseApp } from '../app/base.js';
import type { FetchState } from '../fetch/fetch-state.js';
import type { Pipeline } from '../base-pipeline.js';
import {
ASTRO_ERROR_HEADER,
REROUTE_DIRECTIVE_HEADER,
REWRITE_DIRECTIVE_HEADER_KEY,
REWRITE_DIRECTIVE_HEADER_VALUE,
ROUTE_TYPE_HEADER,
} from '../constants.js';
import { ASTRO_ERROR_HEADER } from '../constants.js';
import { getCookiesFromResponse } from '../cookies/response.js';

// Shared empty-slots object so we don't allocate `{}` on every render for
Expand Down Expand Up @@ -40,6 +34,7 @@ export class PagesHandler {
async handle(state: FetchState, ctx: APIContext): Promise<Response> {
const pipeline = this.#pipeline;
const { logger, streaming } = pipeline;
state.resetResponseMetadata();

let response: Response;

Expand All @@ -51,6 +46,7 @@ export class PagesHandler {
ctx,
state.routeData!.prerender,
logger,
state,
);
break;
}
Expand All @@ -75,21 +71,19 @@ export class PagesHandler {
}

// Signal to the i18n middleware to maybe act on this response
response.headers.set(ROUTE_TYPE_HEADER, 'page');
state.responseRouteType = 'page';
// Signal to the error-page-rerouting infra to let this response pass through to avoid loops
if (state.routeData!.route === '/404' || state.routeData!.route === '/500') {
response.headers.set(REROUTE_DIRECTIVE_HEADER, 'no');
}
if (state.isRewriting) {
response.headers.set(REWRITE_DIRECTIVE_HEADER_KEY, REWRITE_DIRECTIVE_HEADER_VALUE);
state.skipErrorReroute = true;
}
break;
}
case 'redirect': {
return new Response(null, { status: 404, headers: { [ASTRO_ERROR_HEADER]: 'true' } });
}
case 'fallback': {
return new Response(null, { status: 500, headers: { [ROUTE_TYPE_HEADER]: 'fallback' } });
state.responseRouteType = 'fallback';
return new Response(null, { status: 500 });
}
}
// We need to merge the cookies from the response back into the cookies
Expand Down
Loading
Loading