Repository: sveltejs/kit
Pull request #4204 (2022-03-04 00:38:21)

[chore] downgrade Windows version
Windows is twice as slow as everything else. It seems to have gotten much worse in the past week

There are open issues suggesting it might be a regression in `windows-latest`
- https://github.com/actions/virtual-environments/issues/5166
- https://github.com/actions/virtual-environments/issues/5158

changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 5abccbb4c9c1d6726354248e5f8847bee87c8757

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/sveltejs/kit/new/win-2019?filename=.changeset/cyan-dragons-clean.md&value=---%0A%0A---%0A%0A%5Bchore%5D%20downgrade%20Windows%20version%0A)



Patch:
diff --git a/.changeset/gorgeous-beans-glow.md b/.changeset/gorgeous-beans-glow.md
new file mode 100644
index 000000000..6ef3bba3b
--- /dev/null
+++ b/.changeset/gorgeous-beans-glow.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] expose entire config to adapters, rather than just appDir and trailingSlash
diff --git a/.changeset/khaki-dolls-cough.md b/.changeset/khaki-dolls-cough.md
new file mode 100644
index 000000000..2e85df32d
--- /dev/null
+++ b/.changeset/khaki-dolls-cough.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] replace builder.prerender() with builder.writePrerendered() and builder.prerendered
diff --git a/.changeset/large-berries-exercise.md b/.changeset/large-berries-exercise.md
new file mode 100644
index 000000000..4e72f454e
--- /dev/null
+++ b/.changeset/large-berries-exercise.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] prerender pages during build, regardless of adapter
diff --git a/.changeset/modern-toys-appear.md b/.changeset/modern-toys-appear.md
new file mode 100644
index 000000000..3ca8cd419
--- /dev/null
+++ b/.changeset/modern-toys-appear.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Add config.kit.prerender.default option
diff --git a/.changeset/nine-walls-shake.md b/.changeset/nine-walls-shake.md
new file mode 100644
index 000000000..f222efb4a
--- /dev/null
+++ b/.changeset/nine-walls-shake.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Use prerendered pages in svelte-kit preview
diff --git a/.changeset/shaggy-days-cheat.md b/.changeset/shaggy-days-cheat.md
new file mode 100644
index 000000000..bb6f4b5a8
--- /dev/null
+++ b/.changeset/shaggy-days-cheat.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Make prerendered paths available to service workers
diff --git a/.changeset/sour-needles-compete.md b/.changeset/sour-needles-compete.md
new file mode 100644
index 000000000..7008b4a5c
--- /dev/null
+++ b/.changeset/sour-needles-compete.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Update history immediately before updating DOM
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index aa634e7d9..8568887cb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,7 +32,7 @@ jobs:
       fail-fast: false
       matrix:
         node-version: [14]
-        os: [ubuntu-latest, macOS-latest, windows-latest]
+        os: [ubuntu-latest, macOS-latest, windows-2019]
         include:
           - node-version: 16
             os: ubuntu-latest
diff --git a/.prettierrc b/.prettierrc
index 5e006e353..ff4587b4d 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -13,7 +13,7 @@
 		{
 			"files": [
 				"packages/kit/src/packaging/test/fixtures/**/expected/**/*",
-				"packages/kit/src/core/adapt/prerender/fixtures/**/*"
+				"packages/kit/src/core/build/prerender/fixtures/**/*"
 			],
 			"options": {
 				"requirePragma": true
diff --git a/documentation/docs/09-adapters.md b/documentation/docs/09-adapters.md
index 07957bf1e..d17df5ffb 100644
--- a/documentation/docs/09-adapters.md
+++ b/documentation/docs/09-adapters.md
@@ -99,7 +99,7 @@ The types for `Adapter` and its parameters are available in [types/config.d.ts](
 Within the `adapt` method, there are a number of things that an adapter should do:
 
 - Clear out the build directory
-- Call `builder.prerender({ dest })` to prerender pages
+- Write SvelteKit output with `builder.writeClient`, `builder.writePrerendered`, `builder.writeServer`, and `builder.writeStatic`
 - Output code that:
   - Imports `App` from `${builder.getServerDirectory()}/app.js`
   - Instantiates the app with a manifest generated with `builder.generateManifest({ relativePath })`
diff --git a/documentation/docs/10-page-options.md b/documentation/docs/10-page-options.md
index 12d8398e9..77a3a954a 100644
--- a/documentation/docs/10-page-options.md
+++ b/documentation/docs/10-page-options.md
@@ -38,9 +38,7 @@ Ordinarily, SvelteKit [hydrates](/docs/appendix#hydration) your server-rendered
 
 It's likely that at least some pages of your app can be represented as a simple HTML file generated at build time. These pages can be [_prerendered_](/docs/appendix#prerendering) by your [adapter](/docs/adapters).
 
-If your entire app is suitable for prerendering, you could use [`adapter-static`](https://github.com/sveltejs/kit/tree/master/packages/adapter-static), which will generate HTML files for every page, plus additional files that are requested by `load` functions in those pages.
-
-In many cases, you'll only want to prerender specific pages in your app. You'll need to annotate these pages:
+Prerendering happens automatically for any page with the `prerender` annotation:
 
 ```html
 <script context="module">
@@ -48,6 +46,16 @@ In many cases, you'll only want to prerender specific pages in your app. You'll
 </script>
 ```
 
+Alternatively, you can set [`confit.kit.prerender.default`](/docs/configuration#prerender) to `true` and prerender everything except pages that are explicitly marked as _not_ prerenderable:
+
+```html
+<script context="module">
+	export const prerender = false;
+</script>
+```
+
+> If your entire app is suitable for prerendering, you can use [`adapter-static`](https://github.com/sveltejs/kit/tree/master/packages/adapter-static), which will output files suitable for use with any static webserver.
+
 The prerenderer will start at the root of your app and generate HTML for any prerenderable pages it finds. Each page is scanned for `<a>` elements that point to other pages that are candidates for prerendering — because of this, you generally don't need to specify which pages should be accessed. If you _do_ need to specify which pages should be accessed by the prerenderer, you can do so with the `entries` option in the [prerender configuration](/docs/configuration#prerender).
 
 #### When not to prerender
diff --git a/documentation/docs/13-configuration.md b/documentation/docs/13-configuration.md
index 9727cff24..f5a1fc42b 100644
--- a/documentation/docs/13-configuration.md
+++ b/documentation/docs/13-configuration.md
@@ -229,6 +229,7 @@ See [Prerendering](/docs/page-options#prerender). An object containing zero or m
 
 - `concurrency` — how many pages can be prerendered simultaneously. JS is single-threaded, but in cases where prerendering performance is network-bound (for example loading content from a remote CMS) this can speed things up by processing other tasks while waiting on the network response
 - `crawl` — determines whether SvelteKit should find pages to prerender by following links from the seed page(s)
+- `default` — set to `true` to prerender every page without `export const prerender = false`
 - `enabled` — set to `false` to disable prerendering altogether
 - `entries` — an array of pages to prerender, or start crawling from (if `crawl: true`). The `*` string includes all non-dynamic routes (i.e. pages with no `[parameters]` )
 - `onError`
diff --git a/packages/adapter-cloudflare-workers/index.js b/packages/adapter-cloudflare-workers/index.js
index 745bbf006..c4b6aa4c6 100644
--- a/packages/adapter-cloudflare-workers/index.js
+++ b/packages/adapter-cloudflare-workers/index.js
@@ -25,9 +25,6 @@ export default function () {
 			builder.rimraf(bucket);
 			builder.rimraf(entrypoint);
 
-			builder.log.info('Prerendering static pages...');
-			const prerendered = await builder.prerender({ dest: bucket });
-
 			builder.log.info('Installing worker dependencies...');
 			builder.copy(`${files}/_package.json`, `${tmp}/package.json`);
 
@@ -49,7 +46,7 @@ export default function () {
 				`${tmp}/manifest.js`,
 				`export const manifest = ${builder.generateManifest({
 					relativePath
-				})};\n\nexport const prerendered = new Set(${JSON.stringify(prerendered.paths)});\n`
+				})};\n\nexport const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});\n`
 			);
 
 			await esbuild.build({
@@ -65,6 +62,7 @@ export default function () {
 			builder.log.minor('Copying assets...');
 			builder.writeClient(bucket);
 			builder.writeStatic(bucket);
+			builder.writePrerendered(bucket);
 		}
 	};
 }
diff --git a/packages/adapter-cloudflare/index.js b/packages/adapter-cloudflare/index.js
index 9af0566f9..ea3005e08 100644
--- a/packages/adapter-cloudflare/index.js
+++ b/packages/adapter-cloudflare/index.js
@@ -18,8 +18,7 @@ export default function (options = {}) {
 
 			builder.writeStatic(dest);
 			builder.writeClient(dest);
-
-			const prerendered = await builder.prerender({ dest });
+			builder.writePrerendered(dest);
 
 			const relativePath = posix.relative(tmp, builder.getServerDirectory());
 
@@ -27,7 +26,7 @@ export default function (options = {}) {
 				`${tmp}/manifest.js`,
 				`export const manifest = ${builder.generateManifest({
 					relativePath
-				})};\n\nexport const prerendered = new Set(${JSON.stringify(prerendered.paths)});\n`
+				})};\n\nexport const prerendered = new Set(${JSON.stringify(builder.prerendered.paths)});\n`
 			);
 
 			builder.copy(`${files}/worker.js`, `${tmp}/_worker.js`, {
diff --git a/packages/adapter-netlify/index.js b/packages/adapter-netlify/index.js
index a37894a88..330071470 100644
--- a/packages/adapter-netlify/index.js
+++ b/packages/adapter-netlify/index.js
@@ -36,11 +36,6 @@ export default function ({ split = false } = {}) {
 
 			builder.log.minor(`Publishing to "${publish}"`);
 
-			builder.log.minor('Prerendering static pages...');
-			await builder.prerender({
-				dest: publish
-			});
-
 			builder.writeServer('.netlify/server');
 
 			// for esbuild, use ESM
@@ -127,6 +122,7 @@ export default function ({ split = false } = {}) {
 			builder.log.minor('Copying assets...');
 			builder.writeStatic(publish);
 			builder.writeClient(publish);
+			builder.writePrerendered(publish);
 
 			builder.log.minor('Writing redirects...');
 			const redirect_file = join(publish, '_redirects');
@@ -138,7 +134,7 @@ export default function ({ split = false } = {}) {
 			builder.copy('_headers', headers_file);
 			appendFileSync(
 				headers_file,
-				`\n\n/${builder.appDir}/*\n  cache-control: public\n  cache-control: immutable\n  cache-control: max-age=31536000\n`
+				`\n\n/${builder.config.kit.appDir}/*\n  cache-control: public\n  cache-control: immutable\n  cache-control: max-age=31536000\n`
 			);
 		}
 	};
diff --git a/packages/adapter-node/index.js b/packages/adapter-node/index.js
index 8fd705951..fc302b17a 100644
--- a/packages/adapter-node/index.js
+++ b/packages/adapter-node/index.js
@@ -34,11 +34,7 @@ export default function ({
 			builder.writeClient(`${out}/client`);
 			builder.writeServer(`${out}/server`);
 			builder.writeStatic(`${out}/static`);
-
-			builder.log.minor('Prerendering static pages');
-			await builder.prerender({
-				dest: `${out}/prerendered`
-			});
+			builder.writePrerendered(`${out}/prerendered`);
 
 			writeFileSync(
 				`${out}/manifest.js`,
diff --git a/packages/adapter-static/index.js b/packages/adapter-static/index.js
index 58483e74b..cb5a05d1d 100644
--- a/packages/adapter-static/index.js
+++ b/packages/adapter-static/index.js
@@ -12,17 +12,18 @@ export default function ({ pages = 'build', assets = pages, fallback, precompres
 		name: '@sveltejs/adapter-static',
 
 		async adapt(builder) {
+			if (!fallback && !builder.config.kit.prerender.default) {
+				builder.log.warn(
+					'You should set `config.kit.prerender.default` to `true` if no fallback is specified'
+				);
+			}
+
 			builder.rimraf(assets);
 			builder.rimraf(pages);
 
 			builder.writeStatic(assets);
 			builder.writeClient(assets);
-
-			await builder.prerender({
-				fallback,
-				all: !fallback,
-				dest: pages
-			});
+			builder.writePrerendered(pages, { fallback });
 
 			if (precompress) {
 				if (pages === assets) {
diff --git a/packages/adapter-vercel/index.js b/packages/adapter-vercel/index.js
index d9c69de89..67c29e4a6 100644
--- a/packages/adapter-vercel/index.js
+++ b/packages/adapter-vercel/index.js
@@ -100,12 +100,6 @@ export default function ({ external = [] } = {}) {
 				lambda: `${dir}/functions/node/render`
 			};
 
-			builder.log.minor('Prerendering static pages...');
-
-			const prerendered = await builder.prerender({
-				dest: `${dir}/static`
-			});
-
 			builder.log.minor('Generating serverless function...');
 
 			const relativePath = posix.relative(tmp, builder.getServerDirectory());
@@ -139,32 +133,36 @@ export default function ({ external = [] } = {}) {
 
 			builder.writeStatic(dirs.static);
 			builder.writeClient(dirs.static);
+			builder.writePrerendered(dirs.static);
 
 			builder.log.minor('Writing routes...');
 
 			builder.mkdirp(`${dir}/config`);
 
-			const prerendered_pages = Array.from(prerendered.pages, ([src, page]) => ({
+			const prerendered_pages = Array.from(builder.prerendered.pages, ([src, page]) => ({
 				src,
 				dest: page.file
 			}));
 
-			const prerendered_redirects = Array.from(prerendered.redirects, ([src, redirect]) => ({
-				src,
-				headers: {
-					Location: redirect.location
-				},
-				status: redirect.status
-			}));
+			const prerendered_redirects = Array.from(
+				builder.prerendered.redirects,
+				([src, redirect]) => ({
+					src,
+					headers: {
+						Location: redirect.location
+					},
+					status: redirect.status
+				})
+			);
 
 			writeFileSync(
 				`${dir}/config/routes.json`,
 				JSON.stringify([
-					...redirects[builder.trailingSlash],
+					...redirects[builder.config.kit.trailingSlash],
 					...prerendered_pages,
 					...prerendered_redirects,
 					{
-						src: `/${builder.appDir}/.+`,
+						src: `/${builder.config.kit.appDir}/.+`,
 						headers: {
 							'cache-control': 'public, immutable, max-age=31536000'
 						}
diff --git a/packages/kit/src/cli.js b/packages/kit/src/cli.js
index 43328e107..d374f6156 100755
--- a/packages/kit/src/cli.js
+++ b/packages/kit/src/cli.js
@@ -5,6 +5,7 @@ import * as ports from 'port-authority';
 import { load_config } from './core/config/index.js';
 import { networkInterfaces, release } from 'os';
 import { coalesce_to_error } from './utils/error.js';
+import { logger } from './core/utils.js';
 
 /** @param {unknown} e */
 function handle_error(e) {
@@ -91,8 +92,10 @@ prog
 			process.env.NODE_ENV = process.env.NODE_ENV || 'production';
 			const config = await load_config();
 
+			const log = logger({ verbose });
+
 			const { build } = await import('./core/build/index.js');
-			const build_data = await build(config);
+			const { build_data, prerendered } = await build(config, { log });
 
 			console.log(
 				`\nRun ${colors.bold().cyan('npm run preview')} to preview your production build locally.`
@@ -100,7 +103,7 @@ prog
 
 			if (config.kit.adapter) {
 				const { adapt } = await import('./core/adapt/index.js');
-				await adapt(config, build_data, { verbose });
+				await adapt(config, build_data, prerendered, { log });
 
 				// this is necessary to close any open db connections, etc
 				process.exit(0);
diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js
index 2a21be674..6c1afb014 100644
--- a/packages/kit/src/core/adapt/builder.js
+++ b/packages/kit/src/core/adapt/builder.js
@@ -1,25 +1,22 @@
 import { copy, rimraf, mkdirp } from '../../utils/filesystem.js';
-import { prerender } from './prerender/prerender.js';
 import { generate_manifest } from '../generate_manifest/index.js';
 
 /**
  * @param {{
  *   config: import('types').ValidatedConfig;
  *   build_data: import('types').BuildData;
+ *   prerendered: import('types').Prerendered;
  *   log: import('types').Logger;
  * }} opts
  * @returns {import('types').Builder}
  */
-export function create_builder({ config, build_data, log }) {
+export function create_builder({ config, build_data, prerendered, log }) {
 	/** @type {Set<string>} */
-	let prerendered_paths;
-
-	let generated_manifest = false;
+	const prerendered_paths = new Set(prerendered.paths);
 
 	/** @param {import('types').RouteData} route */
+	// TODO routes should come pre-filtered
 	function not_prerendered(route) {
-		if (!prerendered_paths) return true;
-
 		if (route.type === 'page' && route.path) {
 			return !prerendered_paths.has(route.path);
 		}
@@ -33,12 +30,10 @@ export function create_builder({ config, build_data, log }) {
 		mkdirp,
 		copy,
 
-		appDir: config.kit.appDir,
-		trailingSlash: config.kit.trailingSlash,
+		config,
+		prerendered,
 
 		createEntries(fn) {
-			generated_manifest = true;
-
 			const { routes } = build_data.manifest_data;
 
 			/** @type {import('types').RouteDefinition[]} */
@@ -99,20 +94,24 @@ export function create_builder({ config, build_data, log }) {
 				if (filtered.size > 0) {
 					complete({
 						generateManifest: ({ relativePath, format }) =>
-							generate_manifest(build_data, relativePath, Array.from(filtered), format)
+							generate_manifest({
+								build_data,
+								relative_path: relativePath,
+								routes: Array.from(filtered),
+								format
+							})
 					});
 				}
 			}
 		},
 
 		generateManifest: ({ relativePath, format }) => {
-			generated_manifest = true;
-			return generate_manifest(
+			return generate_manifest({
 				build_data,
-				relativePath,
-				build_data.manifest_data.routes.filter(not_prerendered),
+				relative_path: relativePath,
+				routes: build_data.manifest_data.routes.filter(not_prerendered),
 				format
-			);
+			});
 		},
 
 		getBuildDirectory(name) {
@@ -137,6 +136,18 @@ export function create_builder({ config, build_data, log }) {
 			});
 		},
 
+		writePrerendered(dest, { fallback } = {}) {
+			const source = `${config.kit.outDir}/output/prerendered`;
+			const files = [...copy(`${source}/pages`, dest), ...copy(`${source}/dependencies`, dest)];
+
+			if (fallback) {
+				files.push(fallback);
+				copy(`${source}/fallback.html`, `${dest}/${fallback}`);
+			}
+
+			return files;
+		},
+
 		writeServer(dest) {
 			return copy(`${config.kit.outDir}/output/server`, dest, {
 				filter: (file) => file[0] !== '.'
@@ -147,25 +158,11 @@ export function create_builder({ config, build_data, log }) {
 			return copy(config.kit.files.assets, dest);
 		},
 
-		async prerender({ all = false, dest, fallback }) {
-			if (generated_manifest) {
-				throw new Error(
-					'Adapters must call prerender(...) before createEntries(...) or generateManifest(...)'
-				);
-			}
-
-			const prerendered = await prerender({
-				out: dest,
-				all,
-				config,
-				build_data,
-				fallback,
-				log
-			});
-
-			prerendered_paths = new Set(prerendered.paths);
-
-			return prerendered;
+		// @ts-expect-error
+		async prerender() {
+			throw new Error(
+				'builder.prerender() has been removed. Prerendering now takes place in the build phase — see builder.prerender and builder.writePrerendered'
+			);
 		}
 	};
 }
diff --git a/packages/kit/src/core/adapt/builder.spec.js b/packages/kit/src/core/adapt/builder.spec.js
index 24370092c..2778cff29 100644
--- a/packages/kit/src/core/adapt/builder.spec.js
+++ b/packages/kit/src/core/adapt/builder.spec.js
@@ -30,6 +30,10 @@ test('copy files', () => {
 		// @ts-expect-error
 		build_data: {},
 		// @ts-expect-error
+		prerendered: {
+			paths: []
+		},
+		// @ts-expect-error
 		log: {}
 	});
 
diff --git a/packages/kit/src/core/adapt/index.js b/packages/kit/src/core/adapt/index.js
index 645b55df7..25d02039f 100644
--- a/packages/kit/src/core/adapt/index.js
+++ b/packages/kit/src/core/adapt/index.js
@@ -1,19 +1,18 @@
 import colors from 'kleur';
-import { logger } from '../utils.js';
 import { create_builder } from './builder.js';
 
 /**
  * @param {import('types').ValidatedConfig} config
  * @param {import('types').BuildData} build_data
- * @param {{ verbose: boolean }} opts
+ * @param {import('types').Prerendered} prerendered
+ * @param {{ log: import('types').Logger }} opts
  */
-export async function adapt(config, build_data, { verbose }) {
+export async function adapt(config, build_data, prerendered, { log }) {
 	const { name, adapt } = config.kit.adapter;
 
 	console.log(colors.bold().cyan(`\n> Using ${name}`));
 
-	const log = logger({ verbose });
-	const builder = create_builder({ config, build_data, log });
+	const builder = create_builder({ config, build_data, prerendered, log });
 	await adapt(builder);
 
 	log.success('done');
diff --git a/packages/kit/src/core/build/build_service_worker.js b/packages/kit/src/core/build/build_service_worker.js
index 965e91459..4d4158952 100644
--- a/packages/kit/src/core/build/build_service_worker.js
+++ b/packages/kit/src/core/build/build_service_worker.js
@@ -2,21 +2,24 @@ import fs from 'fs';
 import vite from 'vite';
 import { s } from '../../utils/misc.js';
 import { deep_merge } from '../../utils/object.js';
+import { normalize_path } from '../../utils/url.js';
 import { print_config_conflicts } from '../config/index.js';
 
 /**
  * @param {{
  *   cwd: string;
  *   assets_base: string;
- *   config: import('types').ValidatedConfig
- *   manifest_data: import('types').ManifestData
+ *   config: import('types').ValidatedConfig;
+ *   manifest_data: import('types').ManifestData;
  *   output_dir: string;
  *   service_worker_entry_file: string | null;
  * }} options
+ * @param {import('types').Prerendered} prerendered
  * @param {import('vite').Manifest} client_manifest
  */
 export async function build_service_worker(
 	{ cwd, assets_base, config, manifest_data, output_dir, service_worker_entry_file },
+	prerendered,
 	client_manifest
 ) {
 	// TODO add any assets referenced in template .html file, e.g. favicon?
@@ -49,6 +52,12 @@ export async function build_service_worker(
 					.map((asset) => `${s(`${config.kit.paths.base}/${asset.file}`)}`)
 					.join(',\n\t\t\t\t')}
 			];
+
+			export const prerendered = [
+				${prerendered.paths
+					.map((path) => s(normalize_path(path, config.kit.trailingSlash)))
+					.join(',\n\t\t\t\t')}
+			];
 		`
 			.replace(/^\t{3}/gm, '')
 			.trim()
diff --git a/packages/kit/src/core/build/index.js b/packages/kit/src/core/build/index.js
index e9ba0d3c3..61def523c 100644
--- a/packages/kit/src/core/build/index.js
+++ b/packages/kit/src/core/build/index.js
@@ -7,12 +7,13 @@ import { generate_manifest } from '../generate_manifest/index.js';
 import { build_service_worker } from './build_service_worker.js';
 import { build_client } from './build_client.js';
 import { build_server } from './build_server.js';
+import { prerender } from './prerender/prerender.js';
 
 /**
  * @param {import('types').ValidatedConfig} config
- * @returns {Promise<import('types').BuildData>}
+ * @param {{ log: import('types').Logger }} opts
  */
-export async function build(config) {
+export async function build(config, { log }) {
 	const cwd = process.cwd(); // TODO is this necessary?
 
 	const build_dir = path.join(config.kit.outDir, 'build');
@@ -44,28 +45,53 @@ export async function build(config) {
 	const client = await build_client(options);
 	const server = await build_server(options, client);
 
-	if (options.service_worker_entry_file) {
-		if (config.kit.paths.assets) {
-			throw new Error('Cannot use service worker alongside config.kit.paths.assets');
-		}
-
-		await build_service_worker(options, client.vite_manifest);
-	}
-
+	/** @type {import('types').BuildData} */
 	const build_data = {
 		app_dir: config.kit.appDir,
 		manifest_data: options.manifest_data,
 		service_worker: options.service_worker_entry_file ? 'service-worker.js' : null, // TODO make file configurable?
 		client,
-		server,
-		static: options.manifest_data.assets.map((asset) => posixify(asset.file)),
-		entries: options.manifest_data.routes
-			.map((route) => (route.type === 'page' ? route.path : ''))
-			.filter(Boolean)
+		server
 	};
 
-	const manifest = `export const manifest = ${generate_manifest(build_data, '.')};\n`;
+	const manifest = `export const manifest = ${generate_manifest({
+		build_data,
+		relative_path: '.',
+		routes: options.manifest_data.routes
+	})};\n`;
 	fs.writeFileSync(`${output_dir}/server/manifest.js`, manifest);
 
-	return build_data;
+	const static_files = options.manifest_data.assets.map((asset) => posixify(asset.file));
+
+	const files = new Set([
+		...static_files,
+		...client.chunks.map((chunk) => `${config.kit.appDir}/${chunk.fileName}`),
+		...client.assets.map((chunk) => `${config.kit.appDir}/${chunk.fileName}`)
+	]);
+
+	// TODO is this right?
+	static_files.forEach((file) => {
+		if (file.endsWith('/index.html')) {
+			files.add(file.slice(0, -11));
+		}
+	});
+
+	const prerendered = await prerender({
+		config,
+		entries: options.manifest_data.routes
+			.map((route) => (route.type === 'page' ? route.path : ''))
+			.filter(Boolean),
+		files,
+		log
+	});
+
+	if (options.service_worker_entry_file) {
+		if (config.kit.paths.assets) {
+			throw new Error('Cannot use service worker alongside config.kit.paths.assets');
+		}
+
+		await build_service_worker(options, prerendered, client.vite_manifest);
+	}
+
+	return { build_data, prerendered };
 }
diff --git a/packages/kit/src/core/adapt/prerender/crawl.js b/packages/kit/src/core/build/prerender/crawl.js
similarity index 100%
rename from packages/kit/src/core/adapt/prerender/crawl.js
rename to packages/kit/src/core/build/prerender/crawl.js
diff --git a/packages/kit/src/core/adapt/prerender/crawl.spec.js b/packages/kit/src/core/build/prerender/crawl.spec.js
similarity index 100%
rename from packages/kit/src/core/adapt/prerender/crawl.spec.js
rename to packages/kit/src/core/build/prerender/crawl.spec.js
diff --git a/packages/kit/src/core/adapt/prerender/fixtures/basic-href/input.html b/packages/kit/src/core/build/prerender/fixtures/basic-href/input.html
similarity index 100%
rename from packages/kit/src/core/adapt/prerender/fixtures/basic-href/input.html
rename to packages/kit/src/core/build/prerender/fixtures/basic-href/input.html
diff --git a/packages/kit/src/core/adapt/prerender/fixtures/basic-href/output.json b/packages/kit/src/core/build/prerender/fixtures/basic-href/output.json
similarity index 100%
rename from packages/kit/src/core/adapt/prerender/fixtures/basic-href/output.json
rename to packages/kit/src/core/build/prerender/fixtures/basic-href/output.json
diff --git a/packages/kit/src/core/adapt/prerender/fixtures/basic-src/input.html b/packages/kit/src/core/build/prerender/fixtures/basic-src/input.html
similarity index 100%
rename from packages/kit/src/core/adapt/prerender/fixtures/basic-src/input.html
rename to packages/kit/src/core/build/prerender/fixtures/basic-src/input.html
diff --git a/packages/kit/src/core/adapt/prerender/fixtures/basic-src/output.json b/packages/kit/src/core/build/prerender/fixtures/basic-src/output.json
similarity index 100%
rename from packages/kit/src/core/adapt/prerender/fixtures/basic-src/output.json
rename to packages/kit/src/core/build/prerender/fixtures/basic-src/output.json
diff --git a/packages/kit/src/core/adapt/prerender/fixtures/basic-srcset/input.html b/packages/kit/src/core/build/prerender/fixtures/basic-srcset/input.html
similarity index 100%
rename from packages/kit/src/core/adapt/prerender/fixtures/basic-srcset/input.html
rename to packages/kit/src/core/build/prerender/fixtures/basic-srcset/input.html
diff --git a/packages/kit/src/core/adapt/prerender/fixtures/basic-srcset/output.json b/packages/kit/src/core/build/prerender/fixtures/basic-srcset/output.json
similarity index 100%
rename from packages/kit/src/core/adapt/prerender/fixtures/basic-srcset/output.json
rename to packages/kit/src/core/build/prerender/fixtures/basic-srcset/output.json
diff --git a/packages/kit/src/core/adapt/prerender/fixtures/include-rel-external/input.html b/packages/kit/src/core/build/prerender/fixtures/include-rel-external/input.html
similarity index 100%
rename from packages/kit/src/core/adapt/prerender/fixtures/include-rel-external/input.html
rename to packages/kit/src/core/build/prerender/fixtures/include-rel-external/input.html
diff --git a/packages/kit/src/core/adapt/prerender/fixtures/include-rel-external/output.json b/packages/kit/src/core/build/prerender/fixtures/include-rel-external/output.json
similarity index 100%
rename from packages/kit/src/core/adapt/prerender/fixtures/include-rel-external/output.json
rename to packages/kit/src/core/build/prerender/fixtures/include-rel-external/output.json
diff --git a/packages/kit/src/core/adapt/prerender/fixtures/unquoted-attributes/input.html b/packages/kit/src/core/build/prerender/fixtures/unquoted-attributes/input.html
similarity index 100%
rename from packages/kit/src/core/adapt/prerender/fixtures/unquoted-attributes/input.html
rename to packages/kit/src/core/build/prerender/fixtures/unquoted-attributes/input.html
diff --git a/packages/kit/src/core/adapt/prerender/fixtures/unquoted-attributes/output.json b/packages/kit/src/core/build/prerender/fixtures/unquoted-attributes/output.json
similarity index 100%
rename from packages/kit/src/core/adapt/prerender/fixtures/unquoted-attributes/output.json
rename to packages/kit/src/core/build/prerender/fixtures/unquoted-attributes/output.json
diff --git a/packages/kit/src/core/adapt/prerender/prerender.js b/packages/kit/src/core/build/prerender/prerender.js
similarity index 85%
rename from packages/kit/src/core/adapt/prerender/prerender.js
rename to packages/kit/src/core/build/prerender/prerender.js
index 9cdab9b90..c21e4da89 100644
--- a/packages/kit/src/core/adapt/prerender/prerender.js
+++ b/packages/kit/src/core/build/prerender/prerender.js
@@ -40,15 +40,13 @@ const REDIRECT = 3;
 
 /**
  * @param {{
- *   out: string;
- *   log: Logger;
  *   config: import('types').ValidatedConfig;
- *   build_data: import('types').BuildData;
- *   fallback?: string;
- *   all: boolean; // disregard `export const prerender = true`
+ *   entries: string[];
+ *   files: Set<string>;
+ *   log: Logger;
  * }} opts
  */
-export async function prerender({ out, log, config, build_data, fallback, all }) {
+export async function prerender({ config, entries, files, log }) {
 	/** @type {import('types').Prerendered} */
 	const prerendered = {
 		pages: new Map(),
@@ -57,7 +55,7 @@ export async function prerender({ out, log, config, build_data, fallback, all })
 		paths: []
 	};
 
-	if (!config.kit.prerender.enabled && !fallback) {
+	if (!config.kit.prerender.enabled) {
 		return prerendered;
 	}
 
@@ -79,18 +77,6 @@ export async function prerender({ out, log, config, build_data, fallback, all })
 
 	const error = normalise_error_handler(log, config.kit.prerender.onError);
 
-	const files = new Set([
-		...build_data.static,
-		...build_data.client.chunks.map((chunk) => `${config.kit.appDir}/${chunk.fileName}`),
-		...build_data.client.assets.map((chunk) => `${config.kit.appDir}/${chunk.fileName}`)
-	]);
-
-	build_data.static.forEach((file) => {
-		if (file.endsWith('/index.html')) {
-			files.add(file.slice(0, -11));
-		}
-	});
-
 	const q = queue(config.kit.prerender.concurrency);
 
 	/**
@@ -145,14 +131,14 @@ export async function prerender({ out, log, config, build_data, fallback, all })
 
 		const response = await server.respond(new Request(`http://sveltekit-prerender${encoded}`), {
 			prerender: {
-				all,
+				default: config.kit.prerender.default,
 				dependencies
 			}
 		});
 
 		const text = await response.text();
 
-		save(response, text, decoded, encoded, referrer, 'linked');
+		save('pages', response, text, decoded, encoded, referrer, 'linked');
 
 		for (const [dependency_path, result] of dependencies) {
 			// this seems circuitous, but using new URL allows us to not care
@@ -162,6 +148,7 @@ export async function prerender({ out, log, config, build_data, fallback, all })
 
 			const body = result.body ?? new Uint8Array(await result.response.arrayBuffer());
 			save(
+				'dependencies',
 				result.response,
 				body,
 				decoded_dependency_path,
@@ -191,6 +178,7 @@ export async function prerender({ out, log, config, build_data, fallback, all })
 	}
 
 	/**
+	 * @param {'pages' | 'dependencies'} category
 	 * @param {Response} response
 	 * @param {string | Uint8Array} body
 	 * @param {string} decoded
@@ -198,13 +186,13 @@ export async function prerender({ out, log, config, build_data, fallback, all })
 	 * @param {string | null} referrer
 	 * @param {'linked' | 'fetched'} referenceType
 	 */
-	function save(response, body, decoded, encoded, referrer, referenceType) {
+	function save(category, response, body, decoded, encoded, referrer, referenceType) {
 		const response_type = Math.floor(response.status / 100);
 		const type = /** @type {string} */ (response.headers.get('content-type'));
 		const is_html = response_type === REDIRECT || type === 'text/html';
 
 		const file = output_filename(decoded, is_html);
-		const dest = `${out}/${file}`;
+		const dest = `${config.kit.outDir}/output/prerendered/${category}/${file}`;
 
 		if (written.has(file)) return;
 		written.add(file);
@@ -268,7 +256,7 @@ export async function prerender({ out, log, config, build_data, fallback, all })
 	if (config.kit.prerender.enabled) {
 		for (const entry of config.kit.prerender.entries) {
 			if (entry === '*') {
-				for (const entry of build_data.entries) {
+				for (const entry of entries) {
 					enqueue(null, normalize_path(config.kit.paths.base + entry, config.kit.trailingSlash)); // TODO can we pre-normalize these?
 				}
 			} else {
@@ -279,19 +267,17 @@ export async function prerender({ out, log, config, build_data, fallback, all })
 		await q.done();
 	}
 
-	if (fallback) {
-		const rendered = await server.respond(new Request('http://sveltekit-prerender/[fallback]'), {
-			prerender: {
-				fallback,
-				all: false,
-				dependencies: new Map()
-			}
-		});
+	const rendered = await server.respond(new Request('http://sveltekit-prerender/[fallback]'), {
+		prerender: {
+			fallback: true,
+			default: false,
+			dependencies: new Map()
+		}
+	});
 
-		const file = join(out, fallback);
-		mkdirp(dirname(file));
-		writeFileSync(file, await rendered.text());
-	}
+	const file = `${config.kit.outDir}/output/prerendered/fallback.html`;
+	mkdirp(dirname(file));
+	writeFileSync(file, await rendered.text());
 
 	return prerendered;
 }
diff --git a/packages/kit/src/core/adapt/prerender/queue.js b/packages/kit/src/core/build/prerender/queue.js
similarity index 100%
rename from packages/kit/src/core/adapt/prerender/queue.js
rename to packages/kit/src/core/build/prerender/queue.js
diff --git a/packages/kit/src/core/adapt/prerender/queue.spec.js b/packages/kit/src/core/build/prerender/queue.spec.js
similarity index 100%
rename from packages/kit/src/core/adapt/prerender/queue.spec.js
rename to packages/kit/src/core/build/prerender/queue.spec.js
diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js
index 03b6b450d..66644f3b1 100644
--- a/packages/kit/src/core/config/index.spec.js
+++ b/packages/kit/src/core/config/index.spec.js
@@ -88,6 +88,7 @@ const get_defaults = (prefix = '') => ({
 			concurrency: 1,
 			crawl: true,
 			createIndexFiles: undefined,
+			default: false,
 			enabled: true,
 			entries: ['*'],
 			force: undefined,
diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js
index d63090abc..a2aa0b0cd 100644
--- a/packages/kit/src/core/config/options.js
+++ b/packages/kit/src/core/config/options.js
@@ -194,6 +194,7 @@ const options = object(
 					(keypath) =>
 						`${keypath} has been removed — it is now controlled by the trailingSlash option. See https://kit.svelte.dev/docs/configuration#trailingslash`
 				),
+				default: boolean(false),
 				enabled: boolean(true),
 				entries: validate(['*'], (input, keypath) => {
 					if (!Array.isArray(input) || !input.every((page) => typeof page === 'string')) {
diff --git a/packages/kit/src/core/generate_manifest/index.js b/packages/kit/src/core/generate_manifest/index.js
index 00c03e029..10536af3e 100644
--- a/packages/kit/src/core/generate_manifest/index.js
+++ b/packages/kit/src/core/generate_manifest/index.js
@@ -2,17 +2,14 @@ import { s } from '../../utils/misc.js';
 import { get_mime_lookup } from '../utils.js';
 
 /**
- * @param {import('types').BuildData} build_data;
- * @param {string} relative_path;
- * @param {import('types').RouteData[]} routes;
- * @param {'esm' | 'cjs'} format
+ * @param {{
+ *   build_data: import('types').BuildData;
+ *   relative_path: string;
+ *   routes: import('types').RouteData[];
+ *   format?: 'esm' | 'cjs'
+ * }} opts
  */
-export function generate_manifest(
-	build_data,
-	relative_path,
-	routes = build_data.manifest_data.routes,
-	format = 'esm'
-) {
+export function generate_manifest({ build_data, relative_path, routes, format = 'esm' }) {
 	/** @typedef {{ index: number, path: string }} LookupEntry */
 	/** @type {Map<string, LookupEntry>} */
 	const bundled_nodes = new Map();
diff --git a/packages/kit/src/core/preview/index.js b/packages/kit/src/core/preview/index.js
index 59c5d1329..579208647 100644
--- a/packages/kit/src/core/preview/index.js
+++ b/packages/kit/src/core/preview/index.js
@@ -7,13 +7,23 @@ import { pathToFileURL } from 'url';
 import { getRequest, setResponse } from '../../node.js';
 import { installFetch } from '../../install-fetch.js';
 import { SVELTE_KIT_ASSETS } from '../constants.js';
+import { normalize_path } from '../../utils/url.js';
 
-/** @param {string} dir */
+/** @typedef {import('http').IncomingMessage} Req */
+/** @typedef {import('http').ServerResponse} Res */
+/** @typedef {(req: Req, res: Res, next: () => void) => void} Handler */
+
+/**
+ * @param {string} dir
+ * @returns {Handler}
+ */
 const mutable = (dir) =>
-	sirv(dir, {
-		etag: true,
-		maxAge: 0
-	});
+	fs.existsSync(dir)
+		? sirv(dir, {
+				etag: true,
+				maxAge: 0
+		  })
+		: (req, res, next) => next();
 
 /**
  * @param {{
@@ -27,96 +37,125 @@ const mutable = (dir) =>
 export async function preview({ port, host, config, https: use_https = false }) {
 	installFetch();
 
+	const { paths } = config.kit;
+	const base = paths.base;
+	const assets = paths.assets ? SVELTE_KIT_ASSETS : paths.base;
+
+	const etag = `"${Date.now()}"`;
+
 	const index_file = join(config.kit.outDir, 'output/server/index.js');
 	const manifest_file = join(config.kit.outDir, 'output/server/manifest.js');
 
 	/** @type {import('types').ServerModule} */
 	const { Server, override } = await import(pathToFileURL(index_file).href);
-
 	const { manifest } = await import(pathToFileURL(manifest_file).href);
 
-	/** @type {import('sirv').RequestHandler} */
-	const static_handler = fs.existsSync(config.kit.files.assets)
-		? mutable(config.kit.files.assets)
-		: (_req, _res, next) => {
-				if (!next) throw new Error('No next() handler is available');
-				return next();
-		  };
-
-	const assets_handler = sirv(join(config.kit.outDir, 'output/client'), {
-		maxAge: 31536000,
-		immutable: true
-	});
-
-	const has_asset_path = !!config.kit.paths.assets;
+	const server = new Server(manifest);
 
 	override({
-		paths: {
-			base: config.kit.paths.base,
-			assets: has_asset_path ? SVELTE_KIT_ASSETS : config.kit.paths.base
-		},
+		paths: { base, assets },
 		prerendering: false,
 		protocol: use_https ? 'https' : 'http',
 		read: (file) => fs.readFileSync(join(config.kit.files.assets, file))
 	});
 
-	const server = new Server(manifest);
-
-	/** @type {import('vite').UserConfig} */
-	const vite_config = (config.kit.vite && (await config.kit.vite())) || {};
+	const handle = compose([
+		// files in `static`
+		scoped(assets, mutable(config.kit.files.assets)),
+
+		// immutable generated client assets
+		scoped(
+			assets,
+			sirv(join(config.kit.outDir, 'output/client'), {
+				maxAge: 31536000,
+				immutable: true
+			})
+		),
+
+		// prerendered dependencies
+		scoped(base, mutable(join(config.kit.outDir, 'output/prerendered/dependencies'))),
+
+		// prerendered pages (we can't just use sirv because we need to
+		// preserve the correct trailingSlash behaviour)
+		scoped(base, (req, res, next) => {
+			let if_none_match_value = req.headers['if-none-match'];
+
+			if (if_none_match_value?.startsWith('W/"')) {
+				if_none_match_value = if_none_match_value.substring(2);
+			}
 
-	const http_server = await get_server(use_https, vite_config, (req, res) => {
-		if (req.url == null) {
-			throw new Error('Invalid request url');
-		}
+			if (if_none_match_value === etag) {
+				res.statusCode = 304;
+				res.end();
+				return;
+			}
 
-		const initial_url = req.url;
+			const { pathname, search } = new URL(/** @type {string} */ (req.url), 'http://dummy');
 
-		const render_handler = async () => {
-			if (initial_url.startsWith(config.kit.paths.base)) {
-				const protocol = use_https ? 'https' : 'http';
-				const host = req.headers['host'];
+			const normalized = normalize_path(pathname, config.kit.trailingSlash);
 
-				let request;
+			if (normalized !== pathname) {
+				res.writeHead(307, {
+					location: normalized + search
+				});
+				res.end();
+				return;
+			}
 
-				try {
-					req.url = initial_url;
-					request = await getRequest(`${protocol}://${host}`, req);
-				} catch (/** @type {any} */ err) {
-					res.statusCode = err.status || 400;
-					return res.end(err.reason || 'Invalid request body');
+			// only treat this as a page if it doesn't include an extension
+			if (pathname === '/' || /\/[^./]+\/?$/.test(pathname)) {
+				const file = join(
+					config.kit.outDir,
+					'output/prerendered/pages' + pathname + (pathname.endsWith('/') ? 'index.html' : '.html')
+				);
+
+				if (fs.existsSync(file)) {
+					res.writeHead(200, {
+						'content-type': 'text/html',
+						etag
+					});
+
+					fs.createReadStream(file).pipe(res);
+					return;
 				}
-
-				setResponse(res, await server.respond(request));
-			} else {
-				res.statusCode = 404;
-				res.end('Not found');
 			}
-		};
-
-		if (has_asset_path) {
-			if (initial_url.startsWith(SVELTE_KIT_ASSETS)) {
-				// custom assets path
-				req.url = initial_url.slice(SVELTE_KIT_ASSETS.length);
-				assets_handler(req, res, () => {
-					static_handler(req, res, render_handler);
-				});
-			} else {
-				render_handler();
-			}
-		} else {
-			if (initial_url.startsWith(config.kit.paths.base)) {
-				req.url = initial_url.slice(config.kit.paths.base.length);
+
+			next();
+		}),
+
+		// SSR
+		scoped(base, async (req, res) => {
+			const protocol = use_https ? 'https' : 'http';
+			const host = req.headers['host'];
+
+			let request;
+
+			try {
+				request = await getRequest(`${protocol}://${host}`, req);
+			} catch (/** @type {any} */ err) {
+				res.statusCode = err.status || 400;
+				return res.end(err.reason || 'Invalid request body');
 			}
-			assets_handler(req, res, () => {
-				static_handler(req, res, render_handler);
-			});
+
+			setResponse(res, await server.respond(request));
+		})
+	]);
+
+	const vite_config = (config.kit.vite && (await config.kit.vite())) || {};
+
+	const http_server = await get_server(use_https, vite_config, (req, res) => {
+		if (req.url == null) {
+			throw new Error('Invalid request url');
 		}
-	});
 
-	await http_server.listen(port, host || '0.0.0.0');
+		handle(req, res);
+	});
 
-	return Promise.resolve(http_server);
+	return new Promise((fulfil) => {
+		http_server.listen(port, host || '0.0.0.0', () => {
+			fulfil(http_server);
+		});
+	});
 }
 
 /**
@@ -142,9 +181,51 @@ async function get_server(use_https, user_config, handler) {
 		}
 	}
 
-	return Promise.resolve(
-		use_https
-			? https.createServer(/** @type {https.ServerOptions} */ (https_options), handler)
-			: http.createServer(handler)
-	);
+	return use_https
+		? https.createServer(/** @type {https.ServerOptions} */ (https_options), handler)
+		: http.createServer(handler);
+}
+
+/** @param {Handler[]} handlers */
+function compose(handlers) {
+	/**
+	 * @param {Req} req
+	 * @param {Res} res
+	 */
+	return (req, res) => {
+		/** @param {number} i */
+		function next(i) {
+			const handler = handlers[i];
+
+			if (handler) {
+				handler(req, res, () => next(i + 1));
+			} else {
+				res.statusCode = 404;
+				res.end('Not found');
+			}
+		}
+
+		next(0);
+	};
+}
+
+/**
+ * @param {string} scope
+ * @param {Handler} handler
+ * @returns {Handler}
+ */
+function scoped(scope, handler) {
+	if (scope === '') return handler;
+
+	return (req, res, next) => {
+		if (req.url?.startsWith(scope)) {
+			req.url = req.url.slice(scope.length);
+			handler(req, res, () => {
+				req.url = scope + req.url;
+				next();
+			});
+		} else {
+			next();
+		}
+	};
 }
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index af24e0c4d..069f1c81c 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -187,7 +187,7 @@ export function create_client({ target, session, base, trailing_slash }) {
 	 * @param {import('./types').NavigationIntent} intent
 	 * @param {string[]} redirect_chain
 	 * @param {boolean} no_cache
-	 * @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean}} [opts]
+	 * @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean, details: { replaceState: boolean, state: any } | null}} [opts]
 	 */
 	async function update(intent, redirect_chain, no_cache, opts) {
 		const current_token = (token = {});
@@ -244,6 +244,13 @@ export function create_client({ target, session, base, trailing_slash }) {
 
 		updating = true;
 
+		if (opts && opts.details) {
+			const { details } = opts;
+			const change = details.replaceState ? 0 : 1;
+			details.state[INDEX_KEY] = current_history_index += change;
+			history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', intent.url);
+		}
+
 		if (started) {
 			current = navigation_result.state;
 
@@ -871,7 +878,8 @@ export function create_client({ target, session, base, trailing_slash }) {
 
 		await update(intent, redirect_chain, false, {
 			scroll,
-			keepfocus
+			keepfocus,
+			details
 		});
 
 		navigating--;
@@ -885,12 +893,6 @@ export function create_client({ target, session, base, trailing_slash }) {
 
 			stores.navigating.set(null);
 		}
-
-		if (details) {
-			const change = details.replaceState ? 0 : 1;
-			details.state[INDEX_KEY] = current_history_index += change;
-			history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', intent.url);
-		}
 	}
 
 	/**
diff --git a/packages/kit/src/runtime/server/page/respond.js b/packages/kit/src/runtime/server/page/respond.js
index 3f610540a..1d5c45475 100644
--- a/packages/kit/src/runtime/server/page/respond.js
+++ b/packages/kit/src/runtime/server/page/respond.js
@@ -67,12 +67,16 @@ export async function respond(opts) {
 
 	let page_config = get_page_config(leaf, options);
 
-	if (!leaf.prerender && state.prerender && !state.prerender.all) {
-		// if the page has `export const prerender = true`, continue,
-		// otherwise bail out at this point
-		return new Response(undefined, {
-			status: 204
-		});
+	if (state.prerender) {
+		// if the page isn't marked as prerenderable (or is explicitly
+		// marked NOT prerenderable, if `prerender.default` is `true`),
+		// then bail out at this point
+		const should_prerender = leaf.prerender ?? state.prerender.default;
+		if (!should_prerender) {
+			return new Response(undefined, {
+				status: 204
+			});
+		}
 	}
 
 	/** @type {Array<Loaded>} */
diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts
index ff8f0fd7f..c71ae90b4 100644
--- a/packages/kit/types/ambient.d.ts
+++ b/packages/kit/types/ambient.d.ts
@@ -228,6 +228,10 @@ declare module '$service-worker' {
 	 * An array of URL strings representing the files in your static directory, or whatever directory is specified by `config.kit.files.assets`. You can customize which files are included from `static` directory using [`config.kit.serviceWorker.files`](/docs/configuration)
 	 */
 	export const files: string[];
+	/**
+	 * An array of pathnames corresponding to prerendered pages and endpoints.
+	 */
+	export const prerendered: string[];
 	/**
 	 * The result of calling `Date.now()` at build time. It's useful for generating unique cache names inside your service worker, so that a later deployment of your app can invalidate old caches.
 	 */
diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts
index 57fca8050..8a085221f 100644
--- a/packages/kit/types/index.d.ts
+++ b/packages/kit/types/index.d.ts
@@ -70,6 +70,7 @@ export interface Config {
 		prerender?: {
 			concurrency?: number;
 			crawl?: boolean;
+			default?: boolean;
 			enabled?: boolean;
 			entries?: string[];
 			onError?: PrerenderOnErrorValue;
diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts
index 139f0a950..e0e10a015 100644
--- a/packages/kit/types/internal.d.ts
+++ b/packages/kit/types/internal.d.ts
@@ -14,6 +14,7 @@ import {
 	HttpMethod,
 	JSONObject,
 	MaybePromise,
+	Prerendered,
 	RequestEvent,
 	RequestOptions,
 	ResolveOptions,
@@ -63,8 +64,6 @@ export interface BuildData {
 		methods: Record<string, HttpMethod[]>;
 		vite_manifest: import('vite').Manifest;
 	};
-	static: string[];
-	entries: string[];
 }
 
 export type CSRComponent = any; // TODO
@@ -149,8 +148,8 @@ export interface PrerenderDependency {
 }
 
 export interface PrerenderOptions {
-	fallback?: string;
-	all: boolean;
+	fallback?: boolean;
+	default: boolean;
 	dependencies: Map<string, PrerenderDependency>;
 }
 
diff --git a/packages/kit/types/private.d.ts b/packages/kit/types/private.d.ts
index 48c0659f0..b4beab62d 100644
--- a/packages/kit/types/private.d.ts
+++ b/packages/kit/types/private.d.ts
@@ -2,7 +2,7 @@
 // but which cannot be imported from `@sveltejs/kit`. Care should
 // be taken to avoid breaking changes when editing this file
 
-import { SSRNodeLoader, SSRRoute } from './internal';
+import { SSRNodeLoader, SSRRoute, ValidatedConfig } from './internal';
 
 export interface AdapterEntry {
 	/**
@@ -35,8 +35,8 @@ export interface Builder {
 	rimraf(dir: string): void;
 	mkdirp(dir: string): void;
 
-	appDir: string;
-	trailingSlash: TrailingSlash;
+	config: ValidatedConfig;
+	prerendered: Prerendered;
 
 	/**
 	 * Create entry points that map to individual functions
@@ -56,6 +56,16 @@ export interface Builder {
 	 * @returns an array of paths corresponding to the files that have been created by the copy
 	 */
 	writeClient(dest: string): string[];
+	/**
+	 *
+	 * @param dest
+	 */
+	writePrerendered(
+		dest: string,
+		opts?: {
+			fallback?: string;
+		}
+	): string[];
 	/**
 	 * @param dest the destination folder to which files should be copied
 	 * @returns an array of paths corresponding to the files that have been created by the copy
@@ -81,8 +91,6 @@ export interface Builder {
 			replace?: Record<string, string>;
 		}
 	): string[];
-
-	prerender(options: { all?: boolean; dest: string; fallback?: string }): Promise<Prerendered>;
 }
 
 // Based on https://github.com/josh-hemphill/csp-typed-directives/blob/latest/src/csp.types.ts


Test patch:
diff --git a/packages/adapter-static/test/apps/prerendered/svelte.config.js b/packages/adapter-static/test/apps/prerendered/svelte.config.js
index 20cd2b3ff..b2c66c3ee 100644
--- a/packages/adapter-static/test/apps/prerendered/svelte.config.js
+++ b/packages/adapter-static/test/apps/prerendered/svelte.config.js
@@ -3,7 +3,11 @@ import adapter from '../../../index.js';
 /** @type {import('@sveltejs/kit').Config} */
 const config = {
 	kit: {
-		adapter: adapter()
+		adapter: adapter(),
+
+		prerender: {
+			default: true
+		}
 	}
 };
 
diff --git a/packages/kit/test/apps/basics/src/routes/iframes/index.svelte b/packages/kit/test/apps/basics/src/routes/iframes/index.svelte
new file mode 100644
index 000000000..b1c6dd864
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/iframes/index.svelte
@@ -0,0 +1,1 @@
+<a href="/iframes/nested/parent">parent</a>
diff --git a/packages/kit/test/apps/basics/src/routes/iframes/nested/child.svelte b/packages/kit/test/apps/basics/src/routes/iframes/nested/child.svelte
new file mode 100644
index 000000000..95d405856
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/iframes/nested/child.svelte
@@ -0,0 +1,1 @@
+<h1>Hello from the child</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/iframes/nested/parent.svelte b/packages/kit/test/apps/basics/src/routes/iframes/nested/parent.svelte
new file mode 100644
index 000000000..90cc91d81
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/iframes/nested/parent.svelte
@@ -0,0 +1,1 @@
+<iframe title="Child content" src="./child" />
diff --git a/packages/kit/test/apps/basics/svelte.config.js b/packages/kit/test/apps/basics/svelte.config.js
index 857b5c6b7..40df6e81d 100644
--- a/packages/kit/test/apps/basics/svelte.config.js
+++ b/packages/kit/test/apps/basics/svelte.config.js
@@ -3,6 +3,9 @@ import path from 'path';
 /** @type {import('@sveltejs/kit').Config} */
 const config = {
 	kit: {
+		prerender: {
+			onError: 'continue'
+		},
 		vite: {
 			build: {
 				minify: false
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index 821a36ba5..0608e7265 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -2156,6 +2156,15 @@ test.describe.parallel('Routing', () => {
 		await page.evaluate('window.fulfil_navigation && window.fulfil_navigation()');
 		expect(await page.url()).toBe(`${baseURL}/routing/cancellation/b`);
 	});
+
+	test('Relative paths are relative to the current URL', async ({ page, clicknav }) => {
+		await page.goto('/iframes');
+		await clicknav('[href="/iframes/nested/parent"]');
+
+		expect(await page.frameLocator('iframe').locator('h1').textContent()).toBe(
+			'Hello from the child'
+		);
+	});
 });
 
 test.describe.parallel('Session', () => {
diff --git a/packages/kit/test/prerendering/basics/svelte.config.js b/packages/kit/test/prerendering/basics/svelte.config.js
index e2a911987..bad007a33 100644
--- a/packages/kit/test/prerendering/basics/svelte.config.js
+++ b/packages/kit/test/prerendering/basics/svelte.config.js
@@ -7,6 +7,7 @@ const config = {
 		adapter: adapter(),
 
 		prerender: {
+			default: true,
 			onError: 'continue'
 		},
 
diff --git a/packages/kit/test/prerendering/options/svelte.config.js b/packages/kit/test/prerendering/options/svelte.config.js
index 558a6c06f..d84e1d476 100644
--- a/packages/kit/test/prerendering/options/svelte.config.js
+++ b/packages/kit/test/prerendering/options/svelte.config.js
@@ -17,6 +17,10 @@ const config = {
 			assets: 'https://cdn.example.com/stuff'
 		},
 
+		prerender: {
+			default: true
+		},
+
 		trailingSlash: 'always',
 
 		vite: {