From 49872182cec51bfdba245089e7d8929086beaadb Mon Sep 17 00:00:00 2001
From: Matthew Phillips <matthew@skypack.dev>
Date: Tue, 28 Nov 2023 08:23:06 -0500
Subject: [PATCH 1/8] Implements build.format: 'preserve'

---
 .changeset/tame-flies-confess.md              | 14 ++++++
 packages/astro/src/@types/astro.ts            |  6 ++-
 packages/astro/src/core/app/types.ts          |  2 +-
 packages/astro/src/core/build/common.ts       | 28 +++++++++--
 packages/astro/src/core/build/generate.ts     |  6 +--
 .../src/core/build/plugins/plugin-manifest.ts |  4 +-
 packages/astro/src/core/build/util.ts         |  1 +
 packages/astro/src/core/config/schema.ts      |  4 +-
 .../astro/src/core/routing/manifest/create.ts | 18 +++----
 .../core/routing/manifest/serialization.ts    |  1 +
 .../src/vite-plugin-astro-server/route.ts     |  1 +
 .../astro/test/astro-pageDirectoryUrl.test.js | 48 ++++++++++++++-----
 .../page-format/src/pages/nested/index.astro  |  8 ++++
 packages/astro/test/page-format.test.js       |  6 +++
 14 files changed, 111 insertions(+), 36 deletions(-)
 create mode 100644 .changeset/tame-flies-confess.md
 create mode 100644 packages/astro/test/fixtures/page-format/src/pages/nested/index.astro

diff --git a/.changeset/tame-flies-confess.md b/.changeset/tame-flies-confess.md
new file mode 100644
index 000000000000..b096ac8458cd
--- /dev/null
+++ b/.changeset/tame-flies-confess.md
@@ -0,0 +1,14 @@
+---
+'astro': minor
+---
+
+build.format: 'preserve' to preserve source structure in final build
+
+Using `build.format: 'file'`, a method to produce HTML files that are *not* all within folders, it will only produce `index.html` for the base path of `/`. This meant that even if you create explicit index pages with, for example, `page/index.astro`, it would write these out as `page.html`.
+
+This is a bit unexpected, but rather than make a breaking change to `build.format: 'file'` we decided to create a new `build.format: 'preserve'`.
+
+The new format will preserve how the filesystem is structured and make sure that is mirrored over to production. Using this option:
+
+- `page.astro` becomes `page.html`
+- `page/index.astro` becomes `page/index.html`
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 49d3ab991305..114c495a5b6c 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -733,12 +733,13 @@ export interface AstroUserConfig {
 		 * Control the output file format of each page. This value may be set by an adapter for you.
 		 *   - If `'file'`, Astro will generate an HTML file (ex: "/foo.html") for each page.
 		 *   - If `'directory'`, Astro will generate a directory with a nested `index.html` file (ex: "/foo/index.html") for each page.
+		 *   - If `'preserve'`, Astro will generate HTML files exactly as they appear in your source folder (ex: "foo/index.astro" becomes "foo/index.html" but "foo.astro" does not)
 		 *
 		 * ```js
 		 * {
 		 *   build: {
 		 *     // Example: Generate `page.html` instead of `page/index.html` during build.
-		 *     format: 'file'
+		 *     format: 'preserve'
 		 *   }
 		 * }
 		 * ```
@@ -756,7 +757,7 @@ export interface AstroUserConfig {
 		 * - `directory` - Set `trailingSlash: 'always'`
 		 * - `file` - Set `trailingSlash: 'never'`
 		 */
-		format?: 'file' | 'directory';
+		format?: 'file' | 'directory' | 'preserve';
 		/**
 		 * @docs
 		 * @name build.client
@@ -2553,6 +2554,7 @@ export interface RouteData {
 	redirect?: RedirectConfig;
 	redirectRoute?: RouteData;
 	fallbackRoutes: RouteData[];
+	isIndex: boolean;
 }
 
 export type RedirectRouteData = RouteData & {
diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts
index b38f51d64f72..d479bb3f0c13 100644
--- a/packages/astro/src/core/app/types.ts
+++ b/packages/astro/src/core/app/types.ts
@@ -40,7 +40,7 @@ export type SSRManifest = {
 	site?: string;
 	base: string;
 	trailingSlash: 'always' | 'never' | 'ignore';
-	buildFormat: 'file' | 'directory';
+	buildFormat: 'file' | 'directory' | 'preserve';
 	compressHTML: boolean;
 	assetsPrefix?: string;
 	renderers: SSRLoadedRenderer[];
diff --git a/packages/astro/src/core/build/common.ts b/packages/astro/src/core/build/common.ts
index e7efc6439e4e..daa719a3e089 100644
--- a/packages/astro/src/core/build/common.ts
+++ b/packages/astro/src/core/build/common.ts
@@ -1,6 +1,6 @@
 import npath from 'node:path';
 import { fileURLToPath, pathToFileURL } from 'node:url';
-import type { AstroConfig, RouteType } from '../../@types/astro.js';
+import type { AstroConfig, RouteData } from '../../@types/astro.js';
 import { appendForwardSlash } from '../../core/path.js';
 
 const STATUS_CODE_PAGES = new Set(['/404', '/500']);
@@ -17,9 +17,10 @@ function getOutRoot(astroConfig: AstroConfig): URL {
 export function getOutFolder(
 	astroConfig: AstroConfig,
 	pathname: string,
-	routeType: RouteType
+	routeData: RouteData
 ): URL {
 	const outRoot = getOutRoot(astroConfig);
+	const routeType = routeData.type;
 
 	// This is the root folder to write to.
 	switch (routeType) {
@@ -39,6 +40,17 @@ export function getOutFolder(
 					const d = pathname === '' ? pathname : npath.dirname(pathname);
 					return new URL('.' + appendForwardSlash(d), outRoot);
 				}
+				case 'preserve': {
+					let dir;
+					// If the pathname is '' then this is the root index.html
+					// If this is an index route, the folder should be the pathname, not the parent
+					if(pathname === '' || routeData.isIndex) {
+						dir = pathname;
+					} else {
+						dir = npath.dirname(pathname);
+					}
+					return new URL('.' + appendForwardSlash(dir), outRoot);
+				}
 			}
 	}
 }
@@ -47,8 +59,9 @@ export function getOutFile(
 	astroConfig: AstroConfig,
 	outFolder: URL,
 	pathname: string,
-	routeType: RouteType
+	routeData: RouteData
 ): URL {
+	const routeType = routeData.type;
 	switch (routeType) {
 		case 'endpoint':
 			return new URL(npath.basename(pathname), outFolder);
@@ -67,6 +80,15 @@ export function getOutFile(
 					const baseName = npath.basename(pathname);
 					return new URL('./' + (baseName || 'index') + '.html', outFolder);
 				}
+				case 'preserve': {
+					let baseName = npath.basename(pathname);
+					// If there is no base name this is the root route.
+					// If this is an index route, the name should be `index.html`.
+					if(!baseName || routeData.isIndex) {
+						baseName = 'index';
+					}
+					return new URL(`./${baseName}.html`, outFolder);
+				}
 			}
 	}
 }
diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts
index a7f6425cd7e2..fa1d595d2d87 100644
--- a/packages/astro/src/core/build/generate.ts
+++ b/packages/astro/src/core/build/generate.ts
@@ -448,7 +448,7 @@ function getUrlForPath(
 	pathname: string,
 	base: string,
 	origin: string,
-	format: 'directory' | 'file',
+	format: 'directory' | 'file' | 'preserve',
 	routeType: RouteType
 ): URL {
 	/**
@@ -601,8 +601,8 @@ async function generatePath(
 		body = Buffer.from(await response.arrayBuffer());
 	}
 
-	const outFolder = getOutFolder(pipeline.getConfig(), pathname, route.type);
-	const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, route.type);
+	const outFolder = getOutFolder(pipeline.getConfig(), pathname, route);
+	const outFile = getOutFile(pipeline.getConfig(), outFolder, pathname, route);
 	route.distURL = outFile;
 
 	await fs.promises.mkdir(outFolder, { recursive: true });
diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts
index 09408e23af36..80c57e62340b 100644
--- a/packages/astro/src/core/build/plugins/plugin-manifest.ts
+++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts
@@ -176,8 +176,8 @@ function buildManifest(
 		if (!route.prerender) continue;
 		if (!route.pathname) continue;
 
-		const outFolder = getOutFolder(opts.settings.config, route.pathname, route.type);
-		const outFile = getOutFile(opts.settings.config, outFolder, route.pathname, route.type);
+		const outFolder = getOutFolder(opts.settings.config, route.pathname, route);
+		const outFile = getOutFile(opts.settings.config, outFolder, route.pathname, route);
 		const file = outFile.toString().replace(opts.settings.config.build.client.toString(), '');
 		routes.push({
 			file,
diff --git a/packages/astro/src/core/build/util.ts b/packages/astro/src/core/build/util.ts
index fde296a6d246..96a5ec2f2888 100644
--- a/packages/astro/src/core/build/util.ts
+++ b/packages/astro/src/core/build/util.ts
@@ -23,6 +23,7 @@ export function shouldAppendForwardSlash(
 			switch (buildFormat) {
 				case 'directory':
 					return true;
+				case 'preserve':
 				case 'file':
 					return false;
 			}
diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts
index 655db8ed8f5f..434b7d4f713e 100644
--- a/packages/astro/src/core/config/schema.ts
+++ b/packages/astro/src/core/config/schema.ts
@@ -123,7 +123,7 @@ export const AstroConfigSchema = z.object({
 	build: z
 		.object({
 			format: z
-				.union([z.literal('file'), z.literal('directory')])
+				.union([z.literal('file'), z.literal('directory'), z.literal('preserve')])
 				.optional()
 				.default(ASTRO_CONFIG_DEFAULTS.build.format),
 			client: z
@@ -464,7 +464,7 @@ export function createRelativeSchema(cmd: string, fileProtocolRoot: string) {
 		build: z
 			.object({
 				format: z
-					.union([z.literal('file'), z.literal('directory')])
+					.union([z.literal('file'), z.literal('directory'), z.literal('preserve')])
 					.optional()
 					.default(ASTRO_CONFIG_DEFAULTS.build.format),
 				client: z
diff --git a/packages/astro/src/core/routing/manifest/create.ts b/packages/astro/src/core/routing/manifest/create.ts
index 6ab297a5ee22..9b3d69cefc2a 100644
--- a/packages/astro/src/core/routing/manifest/create.ts
+++ b/packages/astro/src/core/routing/manifest/create.ts
@@ -33,10 +33,6 @@ interface Item {
 	routeSuffix: string;
 }
 
-interface ManifestRouteData extends RouteData {
-	isIndex: boolean;
-}
-
 function countOccurrences(needle: string, haystack: string) {
 	let count = 0;
 	for (const hay of haystack) {
@@ -193,7 +189,7 @@ function isSemanticallyEqualSegment(segmentA: RoutePart[], segmentB: RoutePart[]
  *   For example, `/bar` is sorted before `/foo`.
  *   The definition of "alphabetically" is dependent on the default locale of the running system.
  */
-function routeComparator(a: ManifestRouteData, b: ManifestRouteData) {
+function routeComparator(a: RouteData, b: RouteData) {
 	// For sorting purposes, an index route is considered to have one more segment than the URL it represents.
 	const aLength = a.isIndex ? a.segments.length + 1 : a.segments.length;
 	const bLength = b.isIndex ? b.segments.length + 1 : b.segments.length;
@@ -250,9 +246,9 @@ export interface CreateRouteManifestParams {
 function createFileBasedRoutes(
 	{ settings, cwd, fsMod }: CreateRouteManifestParams,
 	logger: Logger
-): ManifestRouteData[] {
+): RouteData[] {
 	const components: string[] = [];
-	const routes: ManifestRouteData[] = [];
+	const routes: RouteData[] = [];
 	const validPageExtensions = new Set<string>([
 		'.astro',
 		...SUPPORTED_MARKDOWN_FILE_EXTENSIONS,
@@ -393,7 +389,7 @@ function createFileBasedRoutes(
 	return routes;
 }
 
-type PrioritizedRoutesData = Record<RoutePriorityOverride, ManifestRouteData[]>;
+type PrioritizedRoutesData = Record<RoutePriorityOverride, RouteData[]>;
 
 function createInjectedRoutes({ settings, cwd }: CreateRouteManifestParams): PrioritizedRoutesData {
 	const { config } = settings;
@@ -639,7 +635,7 @@ export function createRouteManifest(
 
 	const redirectRoutes = createRedirectRoutes(params, routeMap, logger);
 
-	const routes: ManifestRouteData[] = [
+	const routes: RouteData[] = [
 		...injectedRoutes['legacy'].sort(routeComparator),
 		...[...fileBasedRoutes, ...injectedRoutes['normal'], ...redirectRoutes['normal']].sort(
 			routeComparator
@@ -675,8 +671,8 @@ export function createRouteManifest(
 
 		// In this block of code we group routes based on their locale
 
-		// A map like: locale => ManifestRouteData[]
-		const routesByLocale = new Map<string, ManifestRouteData[]>();
+		// A map like: locale => RouteData[]
+		const routesByLocale = new Map<string, RouteData[]>();
 		// This type is here only as a helper. We copy the routes and make them unique, so we don't "process" the same route twice.
 		// The assumption is that a route in the file system belongs to only one locale.
 		const setRoutes = new Set(routes.filter((route) => route.type === 'page'));
diff --git a/packages/astro/src/core/routing/manifest/serialization.ts b/packages/astro/src/core/routing/manifest/serialization.ts
index f70aa84dd0ac..431febcb80f6 100644
--- a/packages/astro/src/core/routing/manifest/serialization.ts
+++ b/packages/astro/src/core/routing/manifest/serialization.ts
@@ -38,5 +38,6 @@ export function deserializeRouteData(rawRouteData: SerializedRouteData): RouteDa
 		fallbackRoutes: rawRouteData.fallbackRoutes.map((fallback) => {
 			return deserializeRouteData(fallback);
 		}),
+		isIndex: rawRouteData.isIndex,
 	};
 }
diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts
index 67a2a4baa3af..65d9a6c92539 100644
--- a/packages/astro/src/vite-plugin-astro-server/route.ts
+++ b/packages/astro/src/vite-plugin-astro-server/route.ts
@@ -234,6 +234,7 @@ export async function handleRoute({
 				type: 'fallback',
 				route: '',
 				fallbackRoutes: [],
+				isIndex: false,
 			};
 			renderContext = await createRenderContext({
 				request,
diff --git a/packages/astro/test/astro-pageDirectoryUrl.test.js b/packages/astro/test/astro-pageDirectoryUrl.test.js
index 978db056aebc..19b75e222951 100644
--- a/packages/astro/test/astro-pageDirectoryUrl.test.js
+++ b/packages/astro/test/astro-pageDirectoryUrl.test.js
@@ -2,21 +2,45 @@ import { expect } from 'chai';
 import { loadFixture } from './test-utils.js';
 
 describe('build format', () => {
-	let fixture;
+	describe('build.format: file', () => {
+		/** @type {import('./test-utils.js').Fixture} */
+		let fixture;
 
-	before(async () => {
-		fixture = await loadFixture({
-			root: './fixtures/astro-page-directory-url',
-			build: {
-				format: 'file',
-			},
+		before(async () => {
+			fixture = await loadFixture({
+				root: './fixtures/astro-page-directory-url',
+				build: {
+					format: 'file',
+				},
+			});
+			await fixture.build();
+		});
+	
+		it('outputs', async () => {
+			expect(await fixture.readFile('/client.html')).to.be.ok;
+			expect(await fixture.readFile('/nested-md.html')).to.be.ok;
+			expect(await fixture.readFile('/nested-astro.html')).to.be.ok;
 		});
-		await fixture.build();
 	});
 
-	it('outputs', async () => {
-		expect(await fixture.readFile('/client.html')).to.be.ok;
-		expect(await fixture.readFile('/nested-md.html')).to.be.ok;
-		expect(await fixture.readFile('/nested-astro.html')).to.be.ok;
+	describe('build.format: preserve', () => {
+		/** @type {import('./test-utils.js').Fixture} */
+		let fixture;
+
+		before(async () => {
+			fixture = await loadFixture({
+				root: './fixtures/astro-page-directory-url',
+				build: {
+					format: 'preserve',
+				},
+			});
+			await fixture.build();
+		});
+	
+		it('outputs', async () => {
+			expect(await fixture.readFile('/client.html')).to.be.ok;
+			expect(await fixture.readFile('/nested-md/index.html')).to.be.ok;
+			expect(await fixture.readFile('/nested-astro/index.html')).to.be.ok;
+		});
 	});
 });
diff --git a/packages/astro/test/fixtures/page-format/src/pages/nested/index.astro b/packages/astro/test/fixtures/page-format/src/pages/nested/index.astro
new file mode 100644
index 000000000000..9c077e2a381b
--- /dev/null
+++ b/packages/astro/test/fixtures/page-format/src/pages/nested/index.astro
@@ -0,0 +1,8 @@
+<html>
+	<head>
+		<title>Testing</title>
+	</head>
+	<body>
+		<h1>Testing</h1>
+	</body>
+</html>
diff --git a/packages/astro/test/page-format.test.js b/packages/astro/test/page-format.test.js
index 2143bf09b263..5c99da7ef6d9 100644
--- a/packages/astro/test/page-format.test.js
+++ b/packages/astro/test/page-format.test.js
@@ -47,6 +47,12 @@ describe('build.format', () => {
 				let $ = cheerio.load(html);
 				expect($('#another').attr('href')).to.equal('/nested/another/');
 			});
+
+			it('index files are written as index.html', async () => {
+				let html = await fixture.readFile('/nested/index.html');
+				let $ = cheerio.load(html);
+				expect($('h1').text()).to.equal('Testing');
+			});
 		});
 	});
 });

From 1eda830564e2e916f48be0f2db5ef94d19c3f8fd Mon Sep 17 00:00:00 2001
From: Matthew Phillips <matthew@skypack.dev>
Date: Mon, 22 Jan 2024 12:23:57 -0500
Subject: [PATCH 2/8] Restructure test

---
 packages/astro/test/page-format.test.js | 25 +++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/packages/astro/test/page-format.test.js b/packages/astro/test/page-format.test.js
index 5c99da7ef6d9..3315b6da82a9 100644
--- a/packages/astro/test/page-format.test.js
+++ b/packages/astro/test/page-format.test.js
@@ -37,6 +37,31 @@ describe('build.format', () => {
 			});
 		});
 
+		describe('Build', () => {
+			before(async () => {
+				await fixture.build();
+			});
+
+			it('relative urls created point to sibling folders', async () => {
+				let html = await fixture.readFile('/nested/page.html');
+				let $ = cheerio.load(html);
+				expect($('#another').attr('href')).to.equal('/nested/another/');
+			});
+		});
+	});
+
+	describe('preserve', () => {
+		/** @type {import('./test-utils').Fixture} */
+		let fixture;
+		before(async () => {
+			fixture = await loadFixture({
+				root: './fixtures/page-format/',
+				build: {
+					format: 'preserve',
+				},
+			});
+		});
+
 		describe('Build', () => {
 			before(async () => {
 				await fixture.build();

From 6b0f1e83a0e62d97ef992b58ab753108a4063f9f Mon Sep 17 00:00:00 2001
From: Matthew Phillips <matthew@skypack.dev>
Date: Wed, 24 Jan 2024 09:36:00 -0500
Subject: [PATCH 3/8] Add a test for base

---
 packages/astro/test/page-format.test.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/packages/astro/test/page-format.test.js b/packages/astro/test/page-format.test.js
index 3315b6da82a9..a022326134d0 100644
--- a/packages/astro/test/page-format.test.js
+++ b/packages/astro/test/page-format.test.js
@@ -55,6 +55,7 @@ describe('build.format', () => {
 		let fixture;
 		before(async () => {
 			fixture = await loadFixture({
+				base: '/test',
 				root: './fixtures/page-format/',
 				build: {
 					format: 'preserve',
@@ -70,7 +71,7 @@ describe('build.format', () => {
 			it('relative urls created point to sibling folders', async () => {
 				let html = await fixture.readFile('/nested/page.html');
 				let $ = cheerio.load(html);
-				expect($('#another').attr('href')).to.equal('/nested/another/');
+				expect($('#another').attr('href')).to.equal('/test/nested/another/');
 			});
 
 			it('index files are written as index.html', async () => {

From e8b0f1b8a126b60f148cd0441ac50866e279939e Mon Sep 17 00:00:00 2001
From: Matthew Phillips <matthew@skypack.dev>
Date: Wed, 24 Jan 2024 12:57:47 -0500
Subject: [PATCH 4/8] Update .changeset/tame-flies-confess.md

Co-authored-by: Florian Lefebvre <contact@florian-lefebvre.dev>
---
 .changeset/tame-flies-confess.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.changeset/tame-flies-confess.md b/.changeset/tame-flies-confess.md
index b096ac8458cd..ed6d946f6e2f 100644
--- a/.changeset/tame-flies-confess.md
+++ b/.changeset/tame-flies-confess.md
@@ -2,7 +2,7 @@
 'astro': minor
 ---
 
-build.format: 'preserve' to preserve source structure in final build
+Adds a new possible value to the `build.format` configuration: 'preserve'. It will preserve your source structure in the final build
 
 Using `build.format: 'file'`, a method to produce HTML files that are *not* all within folders, it will only produce `index.html` for the base path of `/`. This meant that even if you create explicit index pages with, for example, `page/index.astro`, it would write these out as `page.html`.
 

From a01552c43ff166db65d6109817757367d05cbd96 Mon Sep 17 00:00:00 2001
From: Matthew Phillips <matthew@skypack.dev>
Date: Thu, 25 Jan 2024 10:17:08 -0500
Subject: [PATCH 5/8] Add trailing slash + i18n testing

---
 .../fixtures/page-format/src/pages/en/index.astro |  6 ++++++
 .../page-format/src/pages/en/nested/index.astro   |  8 ++++++++
 .../page-format/src/pages/en/nested/page.astro    |  4 ++++
 .../fixtures/page-format/src/pages/index.astro    |  6 ++++++
 packages/astro/test/page-format.test.js           | 15 ++++++++++++---
 5 files changed, 36 insertions(+), 3 deletions(-)
 create mode 100644 packages/astro/test/fixtures/page-format/src/pages/en/index.astro
 create mode 100644 packages/astro/test/fixtures/page-format/src/pages/en/nested/index.astro
 create mode 100644 packages/astro/test/fixtures/page-format/src/pages/en/nested/page.astro
 create mode 100644 packages/astro/test/fixtures/page-format/src/pages/index.astro

diff --git a/packages/astro/test/fixtures/page-format/src/pages/en/index.astro b/packages/astro/test/fixtures/page-format/src/pages/en/index.astro
new file mode 100644
index 000000000000..bcd4c7539a7c
--- /dev/null
+++ b/packages/astro/test/fixtures/page-format/src/pages/en/index.astro
@@ -0,0 +1,6 @@
+---
+---
+<html>
+	<head><title>testing</title></head>
+	<body><h1>testing</h1></body>
+</html>
diff --git a/packages/astro/test/fixtures/page-format/src/pages/en/nested/index.astro b/packages/astro/test/fixtures/page-format/src/pages/en/nested/index.astro
new file mode 100644
index 000000000000..9c077e2a381b
--- /dev/null
+++ b/packages/astro/test/fixtures/page-format/src/pages/en/nested/index.astro
@@ -0,0 +1,8 @@
+<html>
+	<head>
+		<title>Testing</title>
+	</head>
+	<body>
+		<h1>Testing</h1>
+	</body>
+</html>
diff --git a/packages/astro/test/fixtures/page-format/src/pages/en/nested/page.astro b/packages/astro/test/fixtures/page-format/src/pages/en/nested/page.astro
new file mode 100644
index 000000000000..eb67508a7365
--- /dev/null
+++ b/packages/astro/test/fixtures/page-format/src/pages/en/nested/page.astro
@@ -0,0 +1,4 @@
+---
+const another = new URL('./another/', Astro.url);
+---
+<a id="another" href={another.pathname}></a>
diff --git a/packages/astro/test/fixtures/page-format/src/pages/index.astro b/packages/astro/test/fixtures/page-format/src/pages/index.astro
new file mode 100644
index 000000000000..bcd4c7539a7c
--- /dev/null
+++ b/packages/astro/test/fixtures/page-format/src/pages/index.astro
@@ -0,0 +1,6 @@
+---
+---
+<html>
+	<head><title>testing</title></head>
+	<body><h1>testing</h1></body>
+</html>
diff --git a/packages/astro/test/page-format.test.js b/packages/astro/test/page-format.test.js
index a022326134d0..63e5dae833d9 100644
--- a/packages/astro/test/page-format.test.js
+++ b/packages/astro/test/page-format.test.js
@@ -57,9 +57,18 @@ describe('build.format', () => {
 			fixture = await loadFixture({
 				base: '/test',
 				root: './fixtures/page-format/',
+				trailingSlash: 'always',
 				build: {
 					format: 'preserve',
 				},
+				i18n: {
+					locales: ['en'],
+					defaultLocale: 'en',
+					routing: {
+						prefixDefaultLocale: true,
+						redirectToDefaultLocale: true,
+					}
+				}
 			});
 		});
 
@@ -69,13 +78,13 @@ describe('build.format', () => {
 			});
 
 			it('relative urls created point to sibling folders', async () => {
-				let html = await fixture.readFile('/nested/page.html');
+				let html = await fixture.readFile('/en/nested/page.html');
 				let $ = cheerio.load(html);
-				expect($('#another').attr('href')).to.equal('/test/nested/another/');
+				expect($('#another').attr('href')).to.equal('/test/en/nested/another/');
 			});
 
 			it('index files are written as index.html', async () => {
-				let html = await fixture.readFile('/nested/index.html');
+				let html = await fixture.readFile('/en/nested/index.html');
 				let $ = cheerio.load(html);
 				expect($('h1').text()).to.equal('Testing');
 			});

From a712247e06399424c0dc07a54669a86929798578 Mon Sep 17 00:00:00 2001
From: Emanuele Stoppa <my.burning@gmail.com>
Date: Tue, 30 Jan 2024 09:17:14 +0000
Subject: [PATCH 6/8] Update packages/astro/src/@types/astro.ts

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
---
 packages/astro/src/@types/astro.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index 32be50746030..a87aba351fea 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -731,9 +731,9 @@ export interface AstroUserConfig {
 		 * @default `'directory'`
 		 * @description
 		 * Control the output file format of each page. This value may be set by an adapter for you.
-		 *   - If `'file'`, Astro will generate an HTML file (ex: "/foo.html") for each page.
-		 *   - If `'directory'`, Astro will generate a directory with a nested `index.html` file (ex: "/foo/index.html") for each page.
-		 *   - If `'preserve'`, Astro will generate HTML files exactly as they appear in your source folder (ex: "foo/index.astro" becomes "foo/index.html" but "foo.astro" does not)
+		 *   - `'file'`: Astro will generate an HTML file named for each page route. (e.g. `src/pages/about.astro` and `src/pages/about/index.astro` both build the file `/about.html`)
+		 *   - `'directory'`: Astro will generate a directory with a nested `index.html` file for each page. (e.g. `src/pages/about.astro` and `src/pages/about/index.astro` both build the file `/about/index.html`)
+		 *   - `'preserve'`: Astro will generate HTML files exactly as they appear in your source folder. (e.g. `src/pages/about.astro` builds `/about.html` but `src/pages/about/index.astro` builds the file `/about/index.html`) 
 		 *
 		 * ```js
 		 * {

From 85b24bd707b833762d7aab904308e79a1b1cc89f Mon Sep 17 00:00:00 2001
From: Emanuele Stoppa <my.burning@gmail.com>
Date: Tue, 30 Jan 2024 09:18:05 +0000
Subject: [PATCH 7/8] Update .changeset/tame-flies-confess.md

Co-authored-by: Sarah Rainsberger <sarah@rainsberger.ca>
---
 .changeset/tame-flies-confess.md | 14 +++++++++-----
 1 file changed, 9 insertions(+), 5 deletions(-)

diff --git a/.changeset/tame-flies-confess.md b/.changeset/tame-flies-confess.md
index ed6d946f6e2f..f257d283be8e 100644
--- a/.changeset/tame-flies-confess.md
+++ b/.changeset/tame-flies-confess.md
@@ -2,13 +2,17 @@
 'astro': minor
 ---
 
-Adds a new possible value to the `build.format` configuration: 'preserve'. It will preserve your source structure in the final build
+Adds a new `build.format` configuration option: 'preserve'. This option will preserve your source structure in the final build.
 
-Using `build.format: 'file'`, a method to produce HTML files that are *not* all within folders, it will only produce `index.html` for the base path of `/`. This meant that even if you create explicit index pages with, for example, `page/index.astro`, it would write these out as `page.html`.
+The existing configuration options, `file` and `directory`, either build all of your HTML pages as files matching the route name (e.g. `/about.html`) or build all your files as `index.html` within a nested directory structure (e.g. `/about/index.html), respectively. It was not previously possible to control the HTML file built on a per-file basis.
 
-This is a bit unexpected, but rather than make a breaking change to `build.format: 'file'` we decided to create a new `build.format: 'preserve'`.
+One limitation of `build.format: 'file'` is that it cannot create `index.html` files for any individual routes (other than the base path of `/`) while otherwise building named files. Creating explicit index pages within your file structure still generates a file named for the page route (e.g. `src/pages/about/index.astro` builds `/about.html`) when using the `file` configuration option.
+
+Rather than make a breaking change to allow `build.format: 'file'` to be more flexible, we decided to create a new `build.format: 'preserve'`.
 
 The new format will preserve how the filesystem is structured and make sure that is mirrored over to production. Using this option:
 
-- `page.astro` becomes `page.html`
-- `page/index.astro` becomes `page/index.html`
+- `about.astro` becomes `about.html`
+- `about/index.astro` becomes `about/index.html`
+
+See the [`build.format` configuration options reference] for more details

From 8ae668c49fc0bd3cf01b442742b2130a95e49f43 Mon Sep 17 00:00:00 2001
From: Sarah Rainsberger <sarah@rainsberger.ca>
Date: Tue, 30 Jan 2024 11:00:00 -0400
Subject: [PATCH 8/8] tiny punctuation/conjunction nit fixes

---
 .changeset/tame-flies-confess.md   | 4 ++--
 packages/astro/src/@types/astro.ts | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/.changeset/tame-flies-confess.md b/.changeset/tame-flies-confess.md
index f257d283be8e..7b10c23580f8 100644
--- a/.changeset/tame-flies-confess.md
+++ b/.changeset/tame-flies-confess.md
@@ -4,7 +4,7 @@
 
 Adds a new `build.format` configuration option: 'preserve'. This option will preserve your source structure in the final build.
 
-The existing configuration options, `file` and `directory`, either build all of your HTML pages as files matching the route name (e.g. `/about.html`) or build all your files as `index.html` within a nested directory structure (e.g. `/about/index.html), respectively. It was not previously possible to control the HTML file built on a per-file basis.
+The existing configuration options, `file` and `directory`, either build all of your HTML pages as files matching the route name (e.g. `/about.html`) or build all your files as `index.html` within a nested directory structure (e.g. `/about/index.html`), respectively. It was not previously possible to control the HTML file built on a per-file basis.
 
 One limitation of `build.format: 'file'` is that it cannot create `index.html` files for any individual routes (other than the base path of `/`) while otherwise building named files. Creating explicit index pages within your file structure still generates a file named for the page route (e.g. `src/pages/about/index.astro` builds `/about.html`) when using the `file` configuration option.
 
@@ -15,4 +15,4 @@ The new format will preserve how the filesystem is structured and make sure that
 - `about.astro` becomes `about.html`
 - `about/index.astro` becomes `about/index.html`
 
-See the [`build.format` configuration options reference] for more details
+See the [`build.format` configuration options reference] for more details.
diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts
index e06ba735f0a4..88220405b308 100644
--- a/packages/astro/src/@types/astro.ts
+++ b/packages/astro/src/@types/astro.ts
@@ -790,7 +790,7 @@ export interface AstroUserConfig {
 		 * Control the output file format of each page. This value may be set by an adapter for you.
 		 *   - `'file'`: Astro will generate an HTML file named for each page route. (e.g. `src/pages/about.astro` and `src/pages/about/index.astro` both build the file `/about.html`)
 		 *   - `'directory'`: Astro will generate a directory with a nested `index.html` file for each page. (e.g. `src/pages/about.astro` and `src/pages/about/index.astro` both build the file `/about/index.html`)
-		 *   - `'preserve'`: Astro will generate HTML files exactly as they appear in your source folder. (e.g. `src/pages/about.astro` builds `/about.html` but `src/pages/about/index.astro` builds the file `/about/index.html`) 
+		 *   - `'preserve'`: Astro will generate HTML files exactly as they appear in your source folder. (e.g. `src/pages/about.astro` builds `/about.html` and `src/pages/about/index.astro` builds the file `/about/index.html`) 
 		 *
 		 * ```js
 		 * {