Repository: sveltejs/kit (js)

Issue:
Replace `hydrate` and `router` options with `csr`
### Describe the problem

At present, there are four 'page options' — `prerender`, `hydrate`, `router` and `ssr`. As of #6197, these can be set in `+layout[.server].js` files as well as `+page[.server].js` files, which allows us to get rid of the corresponding config options.

`router` is an anomaly, however. It's the only option that has a meaning after the initial render — if you navigate to a page with `router = false`, it will disable the client-side router altogether. There's no getting around it: this is a deeply weird option that basically no-one needs (to understand why it was introduced in the first place, see https://github.com/sveltejs/kit/pull/6197#issuecomment-1225726404).

As of https://github.com/sveltejs/kit/issues/790#issuecomment-1231052222, we'll have a much more sensible approach for disabling the router — `data-sveltekit-reload` applied to a top-level element. This would mean we can get rid of the `router` page option altogether.

This does leave a gap though — right now, if `hydrate` and `router` are both `false`, we don't send any JavaScript with the page at all.

### Describe the proposed solution

We merge `hydrate` and `router` into a single `csr` option that controls whether client-side rendering/routing takes place. (It's convenient that the acronym works for both.) If `csr` is `false`, no JS is sent with the page.

Strictly speaking, this is less flexible than what we have now, but having one-but-not-both be `true` is both kinda useless and currently buggy. This approach would be much simpler, both in implementation and communication — `prerender`, `ssr` and `csr` are a much more logical trio.

### Alternatives considered

Keep the current semantics of `hydrate` and always include the client-side router.

### Importance

would make my life easier

### Additional Information

I'm raising this here as an issue rather than a discussion because I expect approximately three people to be affected by this change.

Pull request:
replace `router`/`hydrate` options with `csr`
closes #6436

TODO:

* [x] update `create-svelte` templates
* [x] docs
* [x] noisy error if a page has `hydrate` or `router` exports

### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
- [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
- [x] This message body should clearly illustrate what problems it solves.
- [x] Ideally, include a test that fails without this PR but passes with it.

### Tests
- [ ] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [x] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0


Patch:
diff --git a/.changeset/sharp-glasses-mix.md b/.changeset/sharp-glasses-mix.md
new file mode 100644
index 000000000..6e6e813c7
--- /dev/null
+++ b/.changeset/sharp-glasses-mix.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] replace `router`/`hydrate` page options with `csr`
diff --git a/documentation/docs/00-introduction.md b/documentation/docs/00-introduction.md
index b8d93d0b6..9416b4545 100644
--- a/documentation/docs/00-introduction.md
+++ b/documentation/docs/00-introduction.md
@@ -12,7 +12,7 @@ title: Introduction
 
 SvelteKit is a framework for building extremely high-performance web apps.
 
-Building an app with all the modern best practices is fiendishly complicated. Those practices include [build optimizations](https://vitejs.dev/guide/features.html#build-optimizations), so that you load only the minimal required code; [offline support](/docs/service-workers); [prefetching](/docs/a-options#sveltekit-prefetch) pages before the user initiates navigation; and [configurable rendering](/docs/page-options) that allows you to generate HTML [on the server](/docs/appendix#ssr) or [in the browser](/docs/page-options#router) at runtime or [at build-time](/docs/page-options#prerender). SvelteKit does all the boring stuff for you so that you can get on with the creative part.
+Building an app with all the modern best practices is fiendishly complicated. Those practices include [build optimizations](https://vitejs.dev/guide/features.html#build-optimizations), so that you load only the minimal required code; [offline support](/docs/service-workers); [prefetching](/docs/a-options#sveltekit-prefetch) pages before the user initiates navigation; and [configurable rendering](/docs/page-options) that allows you to render your app [on the server](/docs/appendix#ssr) or [in the browser](/docs/appendix#csr) at runtime or [at build-time](/docs/page-options#prerender). SvelteKit does all the boring stuff for you so that you can get on with the creative part.
 
 It uses [Vite](https://vitejs.dev/) with a [Svelte plugin](https://github.com/sveltejs/vite-plugin-svelte) to provide a lightning-fast and feature-rich development experience with [Hot Module Replacement (HMR)](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/config.md#hot), where changes to your code are reflected in the browser instantly.
 
diff --git a/documentation/docs/03-routing.md b/documentation/docs/03-routing.md
index 462ff7475..92f6f270e 100644
--- a/documentation/docs/03-routing.md
+++ b/documentation/docs/03-routing.md
@@ -70,9 +70,8 @@ This function runs alongside `+page.svelte`, which means it runs on the server d
 As well as `load`, `page.js` can export values that configure the page's behaviour:
 
 - `export const prerender = true` or `false` or `'auto'`
-- `export const hydrate = true` or `false`
-- `export const router = true` or `false`
 - `export const ssr = true` or `false`
+- `export const csr = true` or `false`
 
 You can find more information about these in [page options](/docs/page-options).
 
@@ -111,7 +110,7 @@ export async function load({ params }) {
 
 During client-side navigation, SvelteKit will load this data from the server, which means that the returned value must be serializable using [devalue](https://github.com/rich-harris/devalue).
 
-Like `+page.js`, `+page.server.js` can export [page options](/docs/page-options) — `prerender`, `hydrate`, `router` and `ssr`.
+Like `+page.js`, `+page.server.js` can export [page options](/docs/page-options) — `prerender`, `ssr` and `csr`.
 
 #### Actions
 
@@ -282,7 +281,7 @@ export function load() {
 }
 ```
 
-If a `+layout.js` exports [page options](/docs/page-options) — `prerender`, `hydrate` `router` and `ssr` — they will be used as defaults for child pages.
+If a `+layout.js` exports [page options](/docs/page-options) — `prerender`, `ssr` and `csr` — they will be used as defaults for child pages.
 
 Data returned from a layout's `load` function is also available to all its child pages:
 
@@ -302,7 +301,7 @@ Data returned from a layout's `load` function is also available to all its child
 
 To run your layout's `load` function on the server, move it to `+layout.server.js`, and change the `LayoutLoad` type to `LayoutServerLoad`.
 
-Like `+layout.js`, `+layout.server.js` can export [page options](/docs/page-options) — `prerender`, `hydrate` `router` and `ssr`.
+Like `+layout.js`, `+layout.server.js` can export [page options](/docs/page-options) — `prerender`, `ssr` and `csr`.
 
 ### +server
 
diff --git a/documentation/docs/09-a-options.md b/documentation/docs/09-a-options.md
index c5184f779..090b603a6 100644
--- a/documentation/docs/09-a-options.md
+++ b/documentation/docs/09-a-options.md
@@ -16,8 +16,6 @@ We can mitigate that by _prefetching_ the data. Adding a `data-sveltekit-prefetc
 
 ...will cause SvelteKit to run the page's `load` function as soon as the user hovers over the link (on a desktop) or touches it (on mobile), rather than waiting for the `click` event to trigger navigation. Typically, this buys us an extra couple of hundred milliseconds, which is the difference between a user interface that feels laggy, and one that feels snappy.
 
-Note that prefetching will not work if the [`router`](/docs/page-options#router) setting is `false`.
-
 You can also programmatically invoke `prefetch` from `$app/navigation`.
 
 ### data-sveltekit-reload
diff --git a/documentation/docs/12-page-options.md b/documentation/docs/12-page-options.md
index e8e44dd0a..4067690ed 100644
--- a/documentation/docs/12-page-options.md
+++ b/documentation/docs/12-page-options.md
@@ -68,35 +68,24 @@ For that reason among others, it's recommended that you always include a file ex
 
 For _pages_, we skirt around this problem by writing `foo/index.html` instead of `foo`.
 
-### hydrate
-
-Ordinarily, SvelteKit [hydrates](/docs/appendix#hydration) your server-rendered HTML into an interactive page. Some pages don't require JavaScript at all — many blog posts and 'about' pages fall into this category. In these cases you can skip hydration through the `hydrate` export:
-
-```js
-/// file: +page.js
-export const hydrate = false;
-```
-
-> If `hydrate` and `router` are both `false`, SvelteKit will not add any JavaScript to the page at all. If [server-side rendering](/docs/hooks#handle) is disabled in `handle`, `hydrate` must be `true` or no content will be rendered.
-
-### router
+Note that this will disable client-side routing for any navigation from this page, regardless of whether the router is already active.
 
-SvelteKit includes a [client-side router](/docs/appendix#routing) that intercepts navigations (from the user clicking on links, or interacting with the back/forward buttons) and updates the page contents, rather than letting the browser handle the navigation by reloading.
+### ssr
 
-In certain circumstances you might need to disable [client-side routing](/docs/appendix#routing) through the `router` export:
+Normally, SvelteKit renders your page on the server first and sends that HTML to the client where it's hydrated. If you set `ssr` to `false`, it renders an empty 'shell' page instead. This is useful if your page is unable to be rendered on the server, but in most situations it's not recommended ([see appendix](/docs/appendix#ssr)).
 
 ```js
 /// file: +page.js
-export const router = false;
+export const ssr = false;
 ```
 
-Note that this will disable client-side routing for any navigation from this page, regardless of whether the router is already active.
+### csr
 
-### ssr
-
-Normally, SvelteKit renders your page on the server first and sends that HTML to the client where it's hydrated. If you set `ssr` to `false`, it renders an empty 'shell' page instead. This is useful if your page accesses browser-only methods or objects, but in most situations it's not recommended ([see appendix](/docs/appendix#ssr)).
+Ordinarily, SvelteKit [hydrates](/docs/appendix#hydration) your server-rendered HTML into an interactive client-side-rendered (CSR) page. Some pages don't require JavaScript at all — many blog posts and 'about' pages fall into this category. In these cases you can disable CSR:
 
 ```js
 /// file: +page.js
-export const ssr = false;
+export const csr = false;
 ```
+
+> If both `ssr` and `csr` are `false`, nothing will be rendered!
diff --git a/documentation/docs/17-seo.md b/documentation/docs/17-seo.md
index 2e1d882dc..6642cf509 100644
--- a/documentation/docs/17-seo.md
+++ b/documentation/docs/17-seo.md
@@ -99,14 +99,11 @@ const config = {
 export default config;
 ```
 
-...disabling `hydrate` and `router` in your root `+layout.js`/`+layout.server.js`...
+...disabling `csr` in your root `+layout.js`/`+layout.server.js`...
 
 ```js
 /// file: src/routes/+layout.server.js
-// the combination of these options
-// disables JavaScript
-export const hydrate = false;
-export const router = false;
+export const csr = false;
 ```
 
 ...and transforming the HTML using `transformPageChunk` along with `transform` imported from `@sveltejs/amp`:
diff --git a/packages/create-svelte/templates/default/src/routes/about/+page.ts b/packages/create-svelte/templates/default/src/routes/about/+page.ts
index d08ebec1b..3e13462cd 100644
--- a/packages/create-svelte/templates/default/src/routes/about/+page.ts
+++ b/packages/create-svelte/templates/default/src/routes/about/+page.ts
@@ -1,12 +1,8 @@
-import { browser, dev } from '$app/environment';
+import { dev } from '$app/environment';
 
 // we don't need any JS on this page, though we'll load
 // it in dev so that we get hot module replacement...
-export const hydrate = dev;
-
-// ...but if the client-side router is already loaded
-// (i.e. we came here from elsewhere in the app), use it
-export const router = browser;
+export const csr = dev;
 
 // since there's no dynamic data here, we can prerender
 // it so that it gets served as a static asset in prod
diff --git a/packages/kit/src/exports/hooks/sequence.js b/packages/kit/src/exports/hooks/sequence.js
index c65afca9e..a7048f6d5 100644
--- a/packages/kit/src/exports/hooks/sequence.js
+++ b/packages/kit/src/exports/hooks/sequence.js
@@ -34,12 +34,9 @@ export function sequence(...handlers) {
 						return html;
 					};
 
-					// TODO remove post-https://github.com/sveltejs/kit/pull/6197
-					const ssr = options?.ssr ?? parent_options?.ssr;
-
 					return i < length - 1
-						? apply_handle(i + 1, event, { transformPageChunk, ssr })
-						: resolve(event, { transformPageChunk, ssr });
+						? apply_handle(i + 1, event, { transformPageChunk })
+						: resolve(event, { transformPageChunk });
 				}
 			});
 		}
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index f530f1ac0..5ef45d5d3 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -107,8 +107,6 @@ export function create_client({ target, base, trailing_slash }) {
 	/** @type {import('svelte').SvelteComponent} */
 	let root;
 
-	let router_enabled = true;
-
 	// keeping track of the history index in order to prevent popstate navigation events if needed
 	let current_history_index = history.state?.[INDEX_KEY];
 
@@ -155,22 +153,18 @@ export function create_client({ target, base, trailing_slash }) {
 			url = new URL(url, get_base_uri(document));
 		}
 
-		if (router_enabled) {
-			return navigate({
-				url,
-				scroll: noscroll ? scroll_state() : null,
-				keepfocus,
-				redirect_chain,
-				details: {
-					state,
-					replaceState
-				},
-				accepted: () => {},
-				blocked: () => {}
-			});
-		}
-
-		await native_navigation(url);
+		return navigate({
+			url,
+			scroll: noscroll ? scroll_state() : null,
+			keepfocus,
+			redirect_chain,
+			details: {
+				state,
+				replaceState
+			},
+			accepted: () => {},
+			blocked: () => {}
+		});
 	}
 
 	/** @param {URL} url */
@@ -241,15 +235,7 @@ export function create_client({ target, base, trailing_slash }) {
 					routeId: null
 				});
 			} else {
-				if (router_enabled) {
-					goto(new URL(navigation_result.location, url).href, {}, [
-						...redirect_chain,
-						url.pathname
-					]);
-				} else {
-					await native_navigation(new URL(navigation_result.location, location.href));
-				}
-
+				goto(new URL(navigation_result.location, url).href, {}, [...redirect_chain, url.pathname]);
 				return false;
 			}
 		} else if (navigation_result.props?.page?.status >= 400) {
@@ -354,9 +340,6 @@ export function create_client({ target, base, trailing_slash }) {
 			page = navigation_result.props.page;
 		}
 
-		const leaf_node = navigation_result.state.branch[navigation_result.state.branch.length - 1];
-		router_enabled = leaf_node?.node.shared?.router !== false;
-
 		if (callback) callback();
 
 		updating = false;
@@ -398,10 +381,8 @@ export function create_client({ target, base, trailing_slash }) {
 			});
 		}
 
-		if (router_enabled) {
-			const navigation = { from: null, to: new URL(location.href) };
-			callbacks.after_navigate.forEach((fn) => fn(navigation));
-		}
+		const navigation = { from: null, to: new URL(location.href) };
+		callbacks.after_navigate.forEach((fn) => fn(navigation));
 
 		started = true;
 	}
@@ -1185,8 +1166,6 @@ export function create_client({ target, base, trailing_slash }) {
 
 			/** @param {MouseEvent} event */
 			addEventListener('click', (event) => {
-				if (!router_enabled) return;
-
 				// Adapted from https://github.com/visionmedia/page.js
 				// MIT license https://github.com/visionmedia/page.js#license
 				if (event.button || event.which !== 1) return;
@@ -1256,7 +1235,7 @@ export function create_client({ target, base, trailing_slash }) {
 			});
 
 			addEventListener('popstate', (event) => {
-				if (event.state && router_enabled) {
+				if (event.state) {
 					// if a popstate-driven navigation is cancelled, we need to counteract it
 					// with history.go, which means we end up back here, hence this check
 					if (event.state[INDEX_KEY] === current_history_index) return;
diff --git a/packages/kit/src/runtime/client/start.js b/packages/kit/src/runtime/client/start.js
index aeadbc23d..ac27e445d 100644
--- a/packages/kit/src/runtime/client/start.js
+++ b/packages/kit/src/runtime/client/start.js
@@ -1,19 +1,11 @@
 import { create_client } from './client.js';
 import { init } from './singletons.js';
 import { set_paths } from '../paths.js';
-
-export { set_public_env } from '../env-public.js';
+import { set_public_env } from '../env-public.js';
 
 /**
  * @param {{
- *   paths: {
- *     assets: string;
- *     base: string;
- *   },
- *   target: Element;
- *   route: boolean;
- *   spa: boolean;
- *   trailing_slash: import('types').TrailingSlash;
+ *   env: Record<string, string>;
  *   hydrate: {
  *     status: number;
  *     error: Error | (import('../server/page/types').SerializedHttpError);
@@ -23,9 +15,18 @@ export { set_public_env } from '../env-public.js';
  *     data: Array<import('types').ServerDataNode | null>;
  *     errors: Record<string, any> | null;
  *   };
+ *   paths: {
+ *     assets: string;
+ *     base: string;
+ *   },
+ *   target: Element;
+ *   trailing_slash: import('types').TrailingSlash;
  * }} opts
  */
-export async function start({ paths, target, route, spa, trailing_slash, hydrate }) {
+export async function start({ env, hydrate, paths, target, trailing_slash }) {
+	set_public_env(env);
+	set_paths(paths);
+
 	const client = create_client({
 		target,
 		base: paths.base,
@@ -33,16 +34,14 @@ export async function start({ paths, target, route, spa, trailing_slash, hydrate
 	});
 
 	init({ client });
-	set_paths(paths);
 
 	if (hydrate) {
 		await client._hydrate(hydrate);
+	} else {
+		client.goto(location.href, { replaceState: true });
 	}
 
-	if (route) {
-		if (spa) client.goto(location.href, { replaceState: true });
-		client._start_router();
-	}
+	client._start_router();
 
 	dispatchEvent(new CustomEvent('sveltekit:start'));
 }
diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js
index eaaddfefe..d9cf65be4 100644
--- a/packages/kit/src/runtime/server/index.js
+++ b/packages/kit/src/runtime/server/index.js
@@ -221,7 +221,7 @@ export async function respond(request, options, state) {
 						event,
 						options,
 						state,
-						page_config: { router: true, hydrate: true, ssr: false },
+						page_config: { ssr: false, csr: true },
 						status: 200,
 						error: null,
 						branch: [],
diff --git a/packages/kit/src/runtime/server/page/index.js b/packages/kit/src/runtime/server/page/index.js
index 2691c0f8e..1d05961f9 100644
--- a/packages/kit/src/runtime/server/page/index.js
+++ b/packages/kit/src/runtime/server/page/index.js
@@ -141,9 +141,8 @@ export async function render_page(event, route, page, options, state, resolve_op
 				fetched,
 				cookies,
 				page_config: {
-					hydrate: true,
-					router: true,
-					ssr: false
+					ssr: false,
+					csr: get_option(nodes, 'csr') ?? true
 				},
 				status,
 				error: null,
@@ -273,7 +272,7 @@ export async function render_page(event, route, page, options, state, resolve_op
 								options,
 								state,
 								resolve_opts,
-								page_config: { router: true, hydrate: true, ssr: true },
+								page_config: { ssr: true, csr: true },
 								status,
 								error,
 								branch: compact(branch.slice(0, j + 1)).concat({
@@ -323,8 +322,7 @@ export async function render_page(event, route, page, options, state, resolve_op
 			state,
 			resolve_opts,
 			page_config: {
-				router: get_option(nodes, 'router') ?? true,
-				hydrate: get_option(nodes, 'hydrate') ?? true,
+				csr: get_option(nodes, 'csr') ?? true,
 				ssr: true
 			},
 			status,
diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js
index a966268bf..4ca30dc50 100644
--- a/packages/kit/src/runtime/server/page/render.js
+++ b/packages/kit/src/runtime/server/page/render.js
@@ -23,7 +23,7 @@ const updated = {
  *   cookies: import('set-cookie-parser').Cookie[];
  *   options: import('types').SSROptions;
  *   state: import('types').SSRState;
- *   page_config: { hydrate: boolean, router: boolean; ssr: boolean };
+ *   page_config: { ssr: boolean; csr: boolean };
  *   status: number;
  *   error: HttpError | Error | null;
  *   event: import('types').RequestEvent;
@@ -201,17 +201,11 @@ export async function render_response({
 
 	// prettier-ignore
 	const init_app = `
-		import { set_public_env, start } from ${s(prefixed(entry.file))};
-
-		set_public_env(${s(options.public_env)});
+		import { start } from ${s(prefixed(entry.file))};
 
 		start({
-			target: document.querySelector('[data-sveltekit-hydrate="${target}"]').parentNode,
-			paths: ${s(options.paths)},
-			route: ${!!page_config.router},
-			spa: ${!page_config.ssr},
-			trailing_slash: ${s(options.trailing_slash)},
-			hydrate: ${page_config.ssr && page_config.hydrate ? `{
+			env: ${s(options.public_env)},
+			hydrate: ${page_config.ssr ? `{
 				status: ${status},
 				error: ${error && serialize_error(error, e => e.stack)},
 				node_ids: [${branch.map(({ node }) => node.index).join(', ')}],
@@ -219,7 +213,10 @@ export async function render_response({
 				routeId: ${s(event.routeId)},
 				data: ${serialized.data},
 				errors: ${serialized.errors}
-			}` : 'null'}
+			}` : 'null'},
+			paths: ${s(options.paths)},
+			target: document.querySelector('[data-sveltekit-hydrate="${target}"]').parentNode,
+			trailing_slash: ${s(options.trailing_slash)}
 		});
 	`;
 
@@ -266,7 +263,7 @@ export async function render_response({
 		head += `\n\t<link href="${path}" ${attributes.join(' ')}>`;
 	}
 
-	if (page_config.router || page_config.hydrate) {
+	if (page_config.csr) {
 		for (const dep of modulepreloads) {
 			const path = prefixed(dep);
 			link_header_preloads.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`);
@@ -286,7 +283,7 @@ export async function render_response({
 		body += `\n\t\t<script ${attributes.join(' ')}>${init_app}</script>`;
 	}
 
-	if (page_config.ssr && page_config.hydrate) {
+	if (page_config.ssr && page_config.csr) {
 		/** @type {string[]} */
 		const serialized_data = [];
 
diff --git a/packages/kit/src/runtime/server/page/respond_with_error.js b/packages/kit/src/runtime/server/page/respond_with_error.js
index 658814175..739bc06b3 100644
--- a/packages/kit/src/runtime/server/page/respond_with_error.js
+++ b/packages/kit/src/runtime/server/page/respond_with_error.js
@@ -70,9 +70,8 @@ export async function respond_with_error({ event, options, state, status, error,
 			options,
 			state,
 			page_config: {
-				hydrate: get_option([default_layout], 'hydrate') ?? true,
-				router: get_option([default_layout], 'router') ?? true,
-				ssr
+				ssr,
+				csr: get_option([default_layout], 'csr') ?? true
 			},
 			status,
 			error,
diff --git a/packages/kit/src/runtime/server/utils.js b/packages/kit/src/runtime/server/utils.js
index d44235e9c..26c173ad8 100644
--- a/packages/kit/src/runtime/server/utils.js
+++ b/packages/kit/src/runtime/server/utils.js
@@ -137,7 +137,7 @@ export function data_response(data) {
 }
 
 /**
- * @template {'hydrate' | 'prerender' | 'router' | 'ssr'} Option
+ * @template {'prerender' | 'ssr' | 'csr'} Option
  * @template {Option extends 'prerender' ? import('types').PrerenderOption : boolean} Value
  *
  * @param {Array<import('types').SSRNode | undefined>} nodes
@@ -146,13 +146,20 @@ export function data_response(data) {
  * @returns {Value | undefined}
  */
 export function get_option(nodes, option) {
-	return nodes.reduce(
-		(value, node) =>
-			/** @type {any} TypeScript's too dumb to understand this */ (
-				node?.shared?.[option] ?? node?.server?.[option] ?? value
-			),
-		/** @type {Value | undefined} */ (undefined)
-	);
+	return nodes.reduce((value, node) => {
+		// TODO remove for 1.0
+		for (const thing of [node?.server, node?.shared]) {
+			if (thing && ('router' in thing || 'hydrate' in thing)) {
+				throw new Error(
+					'`export const hydrate` and `export const router` have been replaced with `export const csr`. See https://github.com/sveltejs/kit/pull/6446'
+				);
+			}
+		}
+
+		return /** @type {any} TypeScript's too dumb to understand this */ (
+			node?.shared?.[option] ?? node?.server?.[option] ?? value
+		);
+	}, /** @type {Value | undefined} */ (undefined));
 }
 
 /**
diff --git a/packages/kit/types/internal.d.ts b/packages/kit/types/internal.d.ts
index a2897e6cc..c5669e786 100644
--- a/packages/kit/types/internal.d.ts
+++ b/packages/kit/types/internal.d.ts
@@ -69,8 +69,6 @@ export interface CSRPageNode {
 	component: typeof SvelteComponent;
 	shared: {
 		load?: Load;
-		hydrate?: boolean;
-		router?: boolean;
 	};
 	server: boolean;
 }
@@ -272,18 +270,16 @@ export interface SSRNode {
 
 	shared: {
 		load?: Load;
-		hydrate?: boolean;
 		prerender?: PrerenderOption;
-		router?: boolean;
 		ssr?: boolean;
+		csr?: boolean;
 	};
 
 	server: {
 		load?: ServerLoad;
-		hydrate?: boolean;
 		prerender?: PrerenderOption;
-		router?: boolean;
 		ssr?: boolean;
+		csr?: boolean;
 		POST?: Action;
 		PATCH?: Action;
 		PUT?: Action;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 26d2668dd..aca679ea9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -576,7 +576,7 @@ importers:
       svelte: ^3.48.0
       typescript: ^4.8.2
       vite: ^3.1.0-beta.1
-      vite-imagetools: ^4.0.3
+      vite-imagetools: ^4.0.7
     devDependencies:
       '@sveltejs/adapter-auto': link:../../packages/adapter-auto
       '@sveltejs/adapter-static': link:../../packages/adapter-static
@@ -592,7 +592,7 @@ importers:
       svelte: 3.48.0
       typescript: 4.8.2
       vite: 3.1.0-beta.1
-      vite-imagetools: 4.0.4
+      vite-imagetools: 4.0.7
 
 packages:
 
@@ -2426,8 +2426,8 @@ packages:
     engines: {node: '>= 4'}
     dev: true
 
-  /imagetools-core/3.0.3:
-    resolution: {integrity: sha512-JK7kb0ezkzD27zQgZs7mtRd7DekC+/RTePjIzPsuBAp3S/Z17wNt+6TSEdDJRQSV5xiGV+SOG00weSGVdX3Xig==}
+  /imagetools-core/3.1.1:
+    resolution: {integrity: sha512-LNVnortLEgE0ATHaGZBt3WTccKBz8zNk+NWRHsuyhgMiZ1nCJwAA1I2QcHXUq7OMbsiYm/H5muTv+iSLXM2tiA==}
     engines: {node: '>=12.0.0'}
     dependencies:
       sharp: 0.30.7
@@ -4495,12 +4495,12 @@ packages:
       spdx-expression-parse: 3.0.1
     dev: true
 
-  /vite-imagetools/4.0.4:
-    resolution: {integrity: sha512-ik1Sq4ueKYN2iBjxe3g8YxqM9aul2goQ2z8CuTKKxOG3M1nF63SRqialbXMuJVH8aKJ9A2oGhs1sktCAoo9EBg==}
+  /vite-imagetools/4.0.7:
+    resolution: {integrity: sha512-QyT5J455oD0jzi+bIfP/QHNNTSVdyYeWLXZBS0XeI5hReZ/7U4fD7ypZM1qwEBTEJFte4hDrOsFeyin09wig2A==}
     engines: {node: '>=12.0.0'}
     dependencies:
       '@rollup/pluginutils': 4.2.1
-      imagetools-core: 3.0.3
+      imagetools-core: 3.1.1
       magic-string: 0.26.2
     dev: true
 
diff --git a/sites/kit.svelte.dev/package.json b/sites/kit.svelte.dev/package.json
index d17f0c8a8..2138e12bf 100644
--- a/sites/kit.svelte.dev/package.json
+++ b/sites/kit.svelte.dev/package.json
@@ -22,7 +22,7 @@
 		"svelte": "^3.48.0",
 		"typescript": "^4.8.2",
 		"vite": "^3.1.0-beta.1",
-		"vite-imagetools": "^4.0.3"
+		"vite-imagetools": "^4.0.7"
 	},
 	"type": "module"
 }


Test patch:
diff --git a/packages/kit/test/apps/amp/src/routes/+layout.js b/packages/kit/test/apps/amp/src/routes/+layout.js
index 9ed282a3f..26bb56887 100644
--- a/packages/kit/test/apps/amp/src/routes/+layout.js
+++ b/packages/kit/test/apps/amp/src/routes/+layout.js
@@ -1,2 +1,1 @@
-export const hydrate = false;
-export const router = false;
+export const csr = false;
diff --git a/packages/kit/test/apps/basics/src/routes/no-csr/+page.js b/packages/kit/test/apps/basics/src/routes/no-csr/+page.js
new file mode 100644
index 000000000..911084112
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/no-csr/+page.js
@@ -0,0 +1,7 @@
+export const csr = false;
+
+/** @type {import('./$types').PageLoad} */
+export async function load({ fetch }) {
+	const res = await fetch('/no-csr/data.json');
+	return await res.json();
+}
diff --git a/packages/kit/test/apps/basics/src/routes/no-csr/+page.svelte b/packages/kit/test/apps/basics/src/routes/no-csr/+page.svelte
new file mode 100644
index 000000000..d4ffc8937
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/no-csr/+page.svelte
@@ -0,0 +1,7 @@
+<script>
+	/** @type {import('./$types').PageData} */
+	export let data;
+</script>
+
+<h1>look ma no javascript</h1>
+<p>type: {data.type}</p>
diff --git a/packages/kit/test/apps/basics/src/routes/no-hydrate.json/+server.js b/packages/kit/test/apps/basics/src/routes/no-csr/data.json/+server.js
similarity index 80%
rename from packages/kit/test/apps/basics/src/routes/no-hydrate.json/+server.js
rename to packages/kit/test/apps/basics/src/routes/no-csr/data.json/+server.js
index 0fabcc3f4..adadfe46a 100644
--- a/packages/kit/test/apps/basics/src/routes/no-hydrate.json/+server.js
+++ b/packages/kit/test/apps/basics/src/routes/no-csr/data.json/+server.js
@@ -2,6 +2,6 @@ import { json } from '@sveltejs/kit';
 
 export function GET() {
 	return json({
-		type: 'no-hydrate'
+		type: 'no-csr'
 	});
 }
diff --git a/packages/kit/test/apps/basics/src/routes/no-hydrate/+page.js b/packages/kit/test/apps/basics/src/routes/no-hydrate/+page.js
deleted file mode 100644
index eddde8cf1..000000000
--- a/packages/kit/test/apps/basics/src/routes/no-hydrate/+page.js
+++ /dev/null
@@ -1,11 +0,0 @@
-export const hydrate = false;
-
-/** @type {import('@sveltejs/kit').Load} */
-export async function load({ fetch }) {
-	const res = await fetch('/no-hydrate.json');
-
-	/** @type {any} */
-	const { type } = await res.json();
-
-	return { type };
-}
diff --git a/packages/kit/test/apps/basics/src/routes/no-hydrate/+page.svelte b/packages/kit/test/apps/basics/src/routes/no-hydrate/+page.svelte
deleted file mode 100644
index 6ca14342e..000000000
--- a/packages/kit/test/apps/basics/src/routes/no-hydrate/+page.svelte
+++ /dev/null
@@ -1,12 +0,0 @@
-<script>
-	/** @type {import('./$types').PageData} */
-	export let data;
-
-	let n = 0;
-</script>
-
-<button on:click={() => (n += 1)}>
-	clicks: {n}
-</button>
-
-<a href="/{data.type}/other">other</a>
diff --git a/packages/kit/test/apps/basics/src/routes/no-hydrate/no-js/+page.js b/packages/kit/test/apps/basics/src/routes/no-hydrate/no-js/+page.js
deleted file mode 100644
index f043bbaa8..000000000
--- a/packages/kit/test/apps/basics/src/routes/no-hydrate/no-js/+page.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export const router = false;
-export const hydrate = false;
diff --git a/packages/kit/test/apps/basics/src/routes/no-hydrate/no-js/+page.svelte b/packages/kit/test/apps/basics/src/routes/no-hydrate/no-js/+page.svelte
deleted file mode 100644
index a840a265d..000000000
--- a/packages/kit/test/apps/basics/src/routes/no-hydrate/no-js/+page.svelte
+++ /dev/null
@@ -1,1 +0,0 @@
-<h1>look ma no javascript</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/no-hydrate/other/+page.svelte b/packages/kit/test/apps/basics/src/routes/no-hydrate/other/+page.svelte
deleted file mode 100644
index 2ac0e4037..000000000
--- a/packages/kit/test/apps/basics/src/routes/no-hydrate/other/+page.svelte
+++ /dev/null
@@ -1,1 +0,0 @@
-<a href="/no-hydrate">hydrate</a>
diff --git a/packages/kit/test/apps/basics/src/routes/no-router/+layout.svelte b/packages/kit/test/apps/basics/src/routes/no-router/+layout.svelte
deleted file mode 100644
index ef4b0dbfe..000000000
--- a/packages/kit/test/apps/basics/src/routes/no-router/+layout.svelte
+++ /dev/null
@@ -1,12 +0,0 @@
-<script>
-	let n = 0;
-</script>
-
-<button on:click={() => n += 1}>
-	clicks: {n}
-</button>
-
-<a href="/no-router/a">a</a>
-<a href="/no-router/b">b</a>
-
-<slot></slot>
diff --git a/packages/kit/test/apps/basics/src/routes/no-router/a/+page.js b/packages/kit/test/apps/basics/src/routes/no-router/a/+page.js
deleted file mode 100644
index 90f739775..000000000
--- a/packages/kit/test/apps/basics/src/routes/no-router/a/+page.js
+++ /dev/null
@@ -1,1 +0,0 @@
-export const router = false;
diff --git a/packages/kit/test/apps/basics/src/routes/no-router/a/+page.svelte b/packages/kit/test/apps/basics/src/routes/no-router/a/+page.svelte
deleted file mode 100644
index 57f9d4079..000000000
--- a/packages/kit/test/apps/basics/src/routes/no-router/a/+page.svelte
+++ /dev/null
@@ -1,1 +0,0 @@
-<h1>a</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/no-router/b/+page.svelte b/packages/kit/test/apps/basics/src/routes/no-router/b/+page.svelte
deleted file mode 100644
index e96ed3db8..000000000
--- a/packages/kit/test/apps/basics/src/routes/no-router/b/+page.svelte
+++ /dev/null
@@ -1,1 +0,0 @@
-<h1>b</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/scroll/cross-document/a/+page.js b/packages/kit/test/apps/basics/src/routes/scroll/cross-document/a/+page.js
deleted file mode 100644
index 90f739775..000000000
--- a/packages/kit/test/apps/basics/src/routes/scroll/cross-document/a/+page.js
+++ /dev/null
@@ -1,1 +0,0 @@
-export const router = false;
diff --git a/packages/kit/test/apps/basics/src/routes/scroll/cross-document/a/+page.svelte b/packages/kit/test/apps/basics/src/routes/scroll/cross-document/a/+page.svelte
index 902e783cd..356bf895e 100644
--- a/packages/kit/test/apps/basics/src/routes/scroll/cross-document/a/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/scroll/cross-document/a/+page.svelte
@@ -2,4 +2,4 @@
 
 <div style="height: 200vh; background: teal" />
 
-<a href="/scroll/cross-document/b">b</a>
+<a data-sveltekit-reload href="/scroll/cross-document/b">b</a>
diff --git a/packages/kit/test/apps/basics/src/routes/scroll/cross-document/b/+page.js b/packages/kit/test/apps/basics/src/routes/scroll/cross-document/b/+page.js
deleted file mode 100644
index b33c2aee8..000000000
--- a/packages/kit/test/apps/basics/src/routes/scroll/cross-document/b/+page.js
+++ /dev/null
@@ -1,1 +0,0 @@
-export const router = true;
diff --git a/packages/kit/test/apps/basics/src/routes/scroll/cross-document/c/+page.js b/packages/kit/test/apps/basics/src/routes/scroll/cross-document/c/+page.js
deleted file mode 100644
index b33c2aee8..000000000
--- a/packages/kit/test/apps/basics/src/routes/scroll/cross-document/c/+page.js
+++ /dev/null
@@ -1,1 +0,0 @@
-export const router = true;
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index 1762d0c17..6bc0c73e2 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -414,31 +414,6 @@ test.describe('Load', () => {
 });
 
 test.describe('Page options', () => {
-	test('disables router if router=false', async ({ page, clicknav }) => {
-		await page.goto('/no-router/a');
-
-		await page.click('button');
-		expect(await page.textContent('button')).toBe('clicks: 1');
-
-		await Promise.all([page.waitForNavigation(), page.click('[href="/no-router/b"]')]);
-		expect(await page.textContent('button')).toBe('clicks: 0');
-
-		// wait until hydration before interacting with button
-		await page.waitForSelector('body.started');
-
-		await page.click('button');
-		expect(await page.textContent('button')).toBe('clicks: 1');
-
-		// wait until hydration before attempting backwards client-side navigation
-		await page.waitForSelector('body.started');
-
-		await clicknav('[href="/no-router/a"]');
-		expect(await page.textContent('button')).toBe('clicks: 1');
-
-		await Promise.all([page.waitForNavigation(), page.click('[href="/no-router/b"]')]);
-		expect(await page.textContent('button')).toBe('clicks: 0');
-	});
-
 	test('applies generated component styles with ssr=false (hides announcer)', async ({
 		page,
 		clicknav
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index c409e8f92..6a9bc4cf0 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -1007,44 +1007,24 @@ test.describe('Nested layouts', () => {
 });
 
 test.describe('Page options', () => {
-	test('does not hydrate page with hydrate=false', async ({ page, javaScriptEnabled }) => {
-		await page.goto('/no-hydrate');
-
-		await page.click('button');
-		expect(await page.textContent('button')).toBe('clicks: 0');
-
-		if (javaScriptEnabled) {
-			await Promise.all([page.waitForNavigation(), page.click('[href="/no-hydrate/other"]')]);
-			await Promise.all([page.waitForNavigation(), page.click('[href="/no-hydrate"]')]);
-
-			await page.click('button');
-			expect(await page.textContent('button')).toBe('clicks: 1');
-		} else {
-			// ensure data wasn't inlined
-			expect(
-				await page.evaluate(
-					() => document.querySelectorAll('script[sveltekit\\:data-type="data"]').length
-				)
-			).toBe(0);
-		}
-	});
-
-	test('does not include modulepreload links if JS is completely disabled', async ({
+	test('does not include <script> or <link rel="modulepreload"> with csr=false', async ({
 		page,
 		javaScriptEnabled
 	}) => {
 		if (!javaScriptEnabled) {
-			await page.goto('/no-hydrate/no-js');
+			await page.goto('/no-csr');
 			expect(await page.textContent('h1')).toBe('look ma no javascript');
 			expect(
 				await page.evaluate(() => document.querySelectorAll('link[rel="modulepreload"]').length)
 			).toBe(0);
-		}
-	});
 
-	test('transformPageChunk can change the html output', async ({ page }) => {
-		await page.goto('/transform-page-chunk');
-		expect(await page.getAttribute('meta[name="transform-page"]', 'content')).toBe('Worked!');
+			// ensure data wasn't inlined
+			expect(
+				await page.evaluate(
+					() => document.querySelectorAll('script[sveltekit\\:data-type="data"]').length
+				)
+			).toBe(0);
+		}
 	});
 
 	test('does not SSR page with ssr=false', async ({ page, javaScriptEnabled }) => {
@@ -1062,6 +1042,12 @@ test.describe('Page options', () => {
 		const html = await request.get('/no-ssr/missing');
 		expect(await html.text()).not.toContain('load function was called erroneously');
 	});
+
+	// TODO move this test somewhere more suitable
+	test('transformPageChunk can change the html output', async ({ page }) => {
+		await page.goto('/transform-page-chunk');
+		expect(await page.getAttribute('meta[name="transform-page"]', 'content')).toBe('Worked!');
+	});
 });
 
 test.describe('$app/paths', () => {