Repository: sveltejs/kit
Pull request #7652 (2022-11-14 21:58:36)

Warn if `%sveltekit.body%` is a direct child of `<body>`
#7585. In addition to the warning, it adds `display: contents` to the wrapper elements in the template apps

### 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.
- [ ] 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


changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: 873c256323861587e51fef9c32d9fc3c8ef153f3

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 2 packages</summary>

  | Name          | Type  |
| ------------- | ----- |
| create-svelte | Patch |
| @sveltejs/kit | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/sveltejs/kit/new/gh-7585?filename=.changeset/friendly-pugs-breathe.md&value=---%0A%22create-svelte%22%3A%20patch%0A%22%40sveltejs%2Fkit%22%3A%20patch%0A%22default-template%22%3A%20patch%0A%22skeleton-template%22%3A%20patch%0A---%0A%0AWarn%20if%20%60%25sveltekit.body%25%60%20is%20a%20direct%20child%20of%20%60%3Cbody%3E%60%0A)



Patch:
diff --git a/.changeset/pre.json b/.changeset/pre.json
index 0cfcebc82..015a257b0 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -580,6 +580,7 @@
     "khaki-socks-tan",
     "khaki-wolves-shout",
     "kind-adults-drum",
+    "kind-beans-talk",
     "kind-dragons-poke",
     "kind-ducks-relate",
     "kind-steaks-bake",
@@ -1156,6 +1157,7 @@
     "smooth-shoes-mix",
     "smooth-shrimps-fly",
     "smooth-waves-accept",
+    "smooth-years-speak",
     "soft-bikes-bake",
     "soft-ears-share",
     "soft-gorillas-hear",
@@ -1428,6 +1430,7 @@
     "warm-ads-invite",
     "warm-cameras-fix",
     "warm-cheetahs-wash",
+    "warm-deers-sneeze",
     "warm-dingos-try",
     "warm-frogs-tickle",
     "warm-icons-teach",
diff --git a/.changeset/purple-apes-whisper.md b/.changeset/purple-apes-whisper.md
new file mode 100644
index 000000000..3f9fdfdd0
--- /dev/null
+++ b/.changeset/purple-apes-whisper.md
@@ -0,0 +1,5 @@
+---
+'create-svelte': patch
+---
+
+Add `style="display: contents"` to wrapper element by default
diff --git a/.changeset/slimy-laws-argue.md b/.changeset/slimy-laws-argue.md
new file mode 100644
index 000000000..a2f08128d
--- /dev/null
+++ b/.changeset/slimy-laws-argue.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Warn if `%sveltekit.body%` is direct child of `<body>`
diff --git a/.changeset/smooth-years-speak.md b/.changeset/smooth-years-speak.md
new file mode 100644
index 000000000..570731772
--- /dev/null
+++ b/.changeset/smooth-years-speak.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] use hex/unicode escape sequences for encoding special characters in route directory names
diff --git a/documentation/docs/10-getting-started/30-project-structure.md b/documentation/docs/10-getting-started/30-project-structure.md
index 32ed82007..aa07ccc83 100644
--- a/documentation/docs/10-getting-started/30-project-structure.md
+++ b/documentation/docs/10-getting-started/30-project-structure.md
@@ -42,7 +42,7 @@ The `src` directory contains the meat of your project.
 - `routes` contains the [routes](/docs/routing) of your application
 - `app.html` is your page template — an HTML document containing the following placeholders:
   - `%sveltekit.head%` — `<link>` and `<script>` elements needed by the app, plus any `<svelte:head>` content
-  - `%sveltekit.body%` — the markup for a rendered page. Typically this lives inside a `<div>` or other element, rather than directly inside `<body>`, to prevent bugs caused by browser extensions injecting elements that are then destroyed by the hydration process
+  - `%sveltekit.body%` — the markup for a rendered page. This should live inside a `<div>` or other element, rather than directly inside `<body>`, to prevent bugs caused by browser extensions injecting elements that are then destroyed by the hydration process. SvelteKit will warn you in development if this is not the case
   - `%sveltekit.assets%` — either [`paths.assets`](/docs/configuration#paths), if specified, or a relative path to [`paths.base`](/docs/configuration#paths)
   - `%sveltekit.nonce%` — a [CSP](/docs/configuration#csp) nonce for manually included links and scripts, if used
 - `error.html` (optional) is the page that is rendered when everything else fails. It can contain the following placeholders:
diff --git a/documentation/docs/30-advanced/10-advanced-routing.md b/documentation/docs/30-advanced/10-advanced-routing.md
index 021bfcd06..d60302bb0 100644
--- a/documentation/docs/30-advanced/10-advanced-routing.md
+++ b/documentation/docs/30-advanced/10-advanced-routing.md
@@ -123,27 +123,42 @@ src/routes/[...catchall]/+page.svelte
 
 ### Encoding
 
-Directory names are URI-decoded, meaning that (for example) a directory like `%40[username]` would match characters beginning with `@`:
+Some characters can't be used on the filesystem — `/` on Linux and Mac, `\ / : * ? " < > |` on Windows. The `#` and `%` characters have special meaning in URLs, and the `[ ] ( )` characters have special meaning to SvelteKit, so these also can't be used directly as part of your route.
+
+To use these characters in your routes, you can use hexadecimal escape sequences, which have the format `[x+nn]` where `nn` is a hexadecimal character code:
+
+- `\` — `[x+5c]`
+- `/` — `[x+2f]`
+- `:` — `[x+3a]`
+- `*` — `[x+2a]`
+- `?` — `[x+3f]`
+- `"` — `[x+22]`
+- `<` — `[x+3c]`
+- `>` — `[x+3e]`
+- `|` — `[x+7c]`
+- `#` — `[x+23]`
+- `%` — `[x+25]`
+- `[` — `[x+5b]`
+- `]` — `[x+5d]`
+- `(` — `[x+28]`
+- `)` — `[x+29]`
+
+For example, to create a `/smileys/:-)` route, you would create a `src/routes/smileys/[x+3a]-[x+29]/+page.svelte` file.
+
+You can determine the hexadecimal code for a character with JavaScript:
 
 ```js
-// @filename: ambient.d.ts
-declare global {
-	const assert: {
-		equal: (a: any, b: any) => boolean;
-	};
-}
+':'.charCodeAt(0).toString(16); // '3a', hence '[x+3a]'
+```
 
-export {};
+You can also use Unicode escape sequences. Generally you won't need to as you can use the unencoded character directly, but if — for some reason — you can't have a filename with an emoji in it, for example, then you can use the escaped characters. In other words, these are equivalent:
 
-// @filename: index.js
-// ---cut---
-assert.equal(
-	decodeURIComponent('%40[username]'),
-	'@[username]'
-);
+```
+src/routes/[u+d83e][u+dd2a]/+page.svelte
+src/routes/🤪/+page.svelte
 ```
 
-To express a `%` character, use `%25`, otherwise the result will be malformed.
+The format for a Unicode escape sequence is `[u+nnnn]` where `nnnn` is a valid value between `0000` and `10ffff`. (Unlike JavaScript string escaping, there's no need to use surrogate pairs to represent code points above `ffff`.) To learn more about Unicode encodings, consult [Programming with Unicode](https://unicodebook.readthedocs.io/unicode_encodings.html).
 
 ### Advanced layouts
 
diff --git a/packages/create-svelte/templates/default/src/app.html b/packages/create-svelte/templates/default/src/app.html
index b555f2469..c361a2248 100644
--- a/packages/create-svelte/templates/default/src/app.html
+++ b/packages/create-svelte/templates/default/src/app.html
@@ -7,6 +7,6 @@
 		%sveltekit.head%
 	</head>
 	<body data-sveltekit-prefetch>
-		<div>%sveltekit.body%</div>
+		<div style="display: contents">%sveltekit.body%</div>
 	</body>
 </html>
diff --git a/packages/create-svelte/templates/skeleton/src/app.html b/packages/create-svelte/templates/skeleton/src/app.html
index 5b53ef7e3..30c14fdbf 100644
--- a/packages/create-svelte/templates/skeleton/src/app.html
+++ b/packages/create-svelte/templates/skeleton/src/app.html
@@ -7,6 +7,6 @@
 		%sveltekit.head%
 	</head>
 	<body>
-		<div>%sveltekit.body%</div>
+		<div style="display: contents">%sveltekit.body%</div>
 	</body>
 </html>
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index 0266339e2..64344ce7a 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,15 @@
 # @sveltejs/kit
 
+## 1.0.0-next.546
+
+### Patch Changes
+
+- fix outdated config error ([#7649](https://github.com/sveltejs/kit/pull/7649))
+
+- [breaking] use hex/unicode escape sequences for encoding special characters in route directory names ([#7644](https://github.com/sveltejs/kit/pull/7644))
+
+- fix `beforeNavigate` description — `routeId` -> `route.id` ([#7643](https://github.com/sveltejs/kit/pull/7643))
+
 ## 1.0.0-next.545
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index b338ed25d..86df7ac26 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.0.0-next.545",
+	"version": "1.0.0-next.546",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js
index 95447372a..669ebdda5 100644
--- a/packages/kit/src/core/sync/create_manifest_data/index.js
+++ b/packages/kit/src/core/sync/create_manifest_data/index.js
@@ -102,7 +102,33 @@ function create_routes_and_nodes(cwd, config, fallback) {
 		 * @param {import('types').RouteData | null} parent
 		 */
 		const walk = (depth, id, segment, parent) => {
-			if (/\]\[/.test(id)) {
+			const unescaped = id.replace(/\[([ux])\+([^\]]+)\]/gi, (match, type, code) => {
+				if (match !== match.toLowerCase()) {
+					throw new Error(`Character escape sequence in ${id} must be lowercase`);
+				}
+
+				if (!/[0-9a-f]+/.test(code)) {
+					throw new Error(`Invalid character escape sequence in ${id}`);
+				}
+
+				if (type === 'x') {
+					if (code.length !== 2) {
+						throw new Error(`Hexadecimal escape sequence in ${id} must be two characters`);
+					}
+
+					return String.fromCharCode(parseInt(code, 16));
+				} else {
+					if (code.length < 4 || code.length > 6) {
+						throw new Error(
+							`Unicode escape sequence in ${id} must be between four and six characters`
+						);
+					}
+
+					return String.fromCharCode(parseInt(code, 16));
+				}
+			});
+
+			if (/\]\[/.test(unescaped)) {
 				throw new Error(`Invalid route ${id} — parameters must be separated`);
 			}
 
@@ -110,6 +136,11 @@ function create_routes_and_nodes(cwd, config, fallback) {
 				throw new Error(`Invalid route ${id} — brackets are unbalanced`);
 			}
 
+			if (/#/.test(segment)) {
+				// Vite will barf on files with # in them
+				throw new Error(`Route ${id} should be renamed to ${id.replace(/#/g, '[x+23]')}`);
+			}
+
 			if (/\[\.\.\.\w+\]\/\[\[/.test(id)) {
 				throw new Error(
 					`Invalid route ${id} — an [[optional]] route segment cannot follow a [...rest] route segment`
@@ -469,6 +500,10 @@ function normalize_route_id(id) {
 			// remove groups
 			.replace(/(?<=^|\/)\(.+?\)(?=$|\/)/g, '')
 
+			.replace(/\[[ux]\+([0-9a-f]+)\]/g, (_, x) =>
+				String.fromCharCode(parseInt(x, 16)).replace(/\//g, '%2f')
+			)
+
 			// replace `[param]` with `<*>`, `[param=x]` with `<x>`, and `[[param]]` with `<?*>`
 			.replace(
 				/\[(?:(\[)|(\.\.\.))?.+?(=.+?)?\]\]?/g,
diff --git a/packages/kit/src/core/sync/create_manifest_data/index.spec.js b/packages/kit/src/core/sync/create_manifest_data/index.spec.js
index d0682d39d..e036d3f74 100644
--- a/packages/kit/src/core/sync/create_manifest_data/index.spec.js
+++ b/packages/kit/src/core/sync/create_manifest_data/index.spec.js
@@ -181,32 +181,24 @@ test('succeeds when routes does not exist', () => {
 	]);
 });
 
-// TODO some characters will need to be URL-encoded in the filename
 test('encodes invalid characters', () => {
 	const { nodes, routes } = create('samples/encoding');
 
-	// had to remove ? and " because windows
-
-	// const quote = 'samples/encoding/".svelte';
-	const hash = { component: 'samples/encoding/%23/+page.svelte' };
-	// const question_mark = 'samples/encoding/?.svelte';
+	const quote = { component: 'samples/encoding/[x+22]/+page.svelte' };
+	const hash = { component: 'samples/encoding/[x+23]/+page.svelte' };
+	const question_mark = { component: 'samples/encoding/[x+3f]/+page.svelte' };
 
 	assert.equal(nodes.map(simplify_node), [
 		default_layout,
 		default_error,
-		// quote,
-		hash
-		// question_mark
+		quote,
+		hash,
+		question_mark
 	]);
 
 	assert.equal(
-		routes.map((p) => p.pattern),
-		[
-			/^\/$/,
-			// /^\/%22\/?$/,
-			/^\/%23\/?$/
-			// /^\/%3F\/?$/
-		]
+		routes.map((p) => p.pattern.toString()),
+		[/^\/$/, /^\/%3[Ff]\/?$/, /^\/%23\/?$/, /^\/"\/?$/].map((pattern) => pattern.toString())
 	);
 });
 
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/encoding/%23/+page.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/encoding/[x+22]/+page.svelte
similarity index 100%
rename from packages/kit/src/core/sync/create_manifest_data/test/samples/encoding/%23/+page.svelte
rename to packages/kit/src/core/sync/create_manifest_data/test/samples/encoding/[x+22]/+page.svelte
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/encoding/[x+23]/+page.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/encoding/[x+23]/+page.svelte
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/encoding/[x+3f]/+page.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/encoding/[x+3f]/+page.svelte
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/kit/src/runtime/client/start.js b/packages/kit/src/runtime/client/start.js
index 1809078a1..03b49829c 100644
--- a/packages/kit/src/runtime/client/start.js
+++ b/packages/kit/src/runtime/client/start.js
@@ -19,6 +19,12 @@ export async function start({ env, hydrate, paths, target, trailing_slash }) {
 	set_public_env(env);
 	set_paths(paths);
 
+	if (__SVELTEKIT_DEV__ && target === document.body) {
+		console.warn(
+			`Placing %sveltekit.body% directly inside <body> is not recommended, as your app may break for users who have certain browser extensions installed.\n\nConsider wrapping it in an element:\n\n<div style="display: contents">\n  %sveltekit.body%\n</div>`
+		);
+	}
+
 	const client = create_client({
 		target,
 		base: paths.base,
diff --git a/packages/kit/src/utils/routing.js b/packages/kit/src/utils/routing.js
index bd8d51dda..7991499df 100644
--- a/packages/kit/src/utils/routing.js
+++ b/packages/kit/src/utils/routing.js
@@ -1,6 +1,9 @@
 const param_pattern = /^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/;
 
-/** @param {string} id */
+/**
+ * Creates the regex pattern, extracts parameter names, and generates types for a route
+ * @param {string} id
+ */
 export function parse_route_id(id) {
 	/** @type {string[]} */
 	const names = [];
@@ -21,9 +24,8 @@ export function parse_route_id(id) {
 			: new RegExp(
 					`^${get_route_segments(id)
 						.map((segment, i, segments) => {
-							const decoded_segment = decodeURIComponent(segment);
 							// special case — /[...rest]/ could contain zero segments
-							const rest_match = /^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(decoded_segment);
+							const rest_match = /^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(segment);
 							if (rest_match) {
 								names.push(rest_match[1]);
 								types.push(rest_match[2]);
@@ -31,7 +33,7 @@ export function parse_route_id(id) {
 								return '(?:/(.*))?';
 							}
 							// special case — /[[optional]]/ could contain zero segments
-							const optional_match = /^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(decoded_segment);
+							const optional_match = /^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(segment);
 							if (optional_match) {
 								names.push(optional_match[1]);
 								types.push(optional_match[2]);
@@ -41,14 +43,29 @@ export function parse_route_id(id) {
 
 							const is_last = i === segments.length - 1;
 
-							if (!decoded_segment) {
+							if (!segment) {
 								return;
 							}
 
-							const parts = decoded_segment.split(/\[(.+?)\](?!\])/);
+							const parts = segment.split(/\[(.+?)\](?!\])/);
 							const result = parts
 								.map((content, i) => {
 									if (i % 2) {
+										if (content.startsWith('x+')) {
+											return escape(String.fromCharCode(parseInt(content.slice(2), 16)));
+										}
+
+										if (content.startsWith('u+')) {
+											return escape(
+												String.fromCharCode(
+													...content
+														.slice(2)
+														.split('-')
+														.map((code) => parseInt(code, 16))
+												)
+											);
+										}
+
 										const match = param_pattern.exec(content);
 										if (!match) {
 											throw new Error(
@@ -69,18 +86,7 @@ export function parse_route_id(id) {
 
 									if (is_last && content.includes('.')) add_trailing_slash = false;
 
-									return (
-										content // allow users to specify characters on the file system in an encoded manner
-											.normalize()
-											// '#', '/', and '?' can only appear in URL path segments in an encoded manner.
-											// They will not be touched by decodeURI so need to be encoded here, so
-											// that we can match against them.
-											// We skip '/' since you can't create a file with it on any OS
-											.replace(/#/g, '%23')
-											.replace(/\?/g, '%3F')
-											// escape characters that have special meaning in regex
-											.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
-									); // TODO handle encoding
+									return escape(content);
 								})
 								.join('');
 
@@ -143,3 +149,20 @@ export function exec(match, { names, types, optional }, matchers) {
 
 	return params;
 }
+
+/** @param {string} str */
+function escape(str) {
+	return (
+		str
+			.normalize()
+			// escape [ and ] before escaping other characters, since they are used in the replacements
+			.replace(/[[\]]/g, '\\$&')
+			// replace %, /, ? and # with their encoded versions because decode_pathname leaves them untouched
+			.replace(/%/g, '%25')
+			.replace(/\//g, '%2[Ff]')
+			.replace(/\?/g, '%3[Ff]')
+			.replace(/#/g, '%23')
+			// escape characters that have special meaning in regex
+			.replace(/[.*+?^${}()|\\]/g, '\\$&')
+	);
+}
diff --git a/packages/kit/src/utils/routing.spec.js b/packages/kit/src/utils/routing.spec.js
index 213f07b52..c4b585ac5 100644
--- a/packages/kit/src/utils/routing.spec.js
+++ b/packages/kit/src/utils/routing.spec.js
@@ -58,20 +58,10 @@ const tests = {
 		names: ['id'],
 		types: ['uuid']
 	},
-	'/%23hash-encoded': {
-		pattern: /^\/%23hash-encoded\/?$/,
-		names: [],
-		types: []
-	},
-	'/%40at-encoded/[id]': {
-		pattern: /^\/@at-encoded\/([^/]+?)\/?$/,
+	'/@-symbol/[id]': {
+		pattern: /^\/@-symbol\/([^/]+?)\/?$/,
 		names: ['id'],
 		types: [undefined]
-	},
-	'/%255bdoubly-encoded': {
-		pattern: /^\/%5bdoubly-encoded\/?$/,
-		names: [],
-		types: []
 	}
 };
 


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/encoded/%24[ticker]/+page.svelte b/packages/kit/test/apps/basics/src/routes/encoded/%24[ticker]/+page.svelte
deleted file mode 100644
index a42e298cf..000000000
--- a/packages/kit/test/apps/basics/src/routes/encoded/%24[ticker]/+page.svelte
+++ /dev/null
@@ -1,5 +0,0 @@
-<script>
-	import { page } from '$app/stores';
-</script>
-
-<h1>${$page.params.ticker}</h1>
diff --git a/packages/kit/test/apps/basics/src/routes/encoded/+page.svelte b/packages/kit/test/apps/basics/src/routes/encoded/+page.svelte
index 3b96b33a5..ee0a3ec52 100644
--- a/packages/kit/test/apps/basics/src/routes/encoded/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/encoded/+page.svelte
@@ -3,7 +3,6 @@
 <a href="/encoded/反应">反应</a>
 <a href="/encoded/redirect">Redirect</a>
 <a href="/encoded/@svelte">@svelte</a>
-<a href="/encoded/$SVLT">$SVLT</a>
 <a href="/encoded/test%2520me">test%20me</a>
 <a href="/encoded/test%252fme">test%2fme</a>
 <a href="/encoded/AC%2fDC">AC/DC</a>
diff --git a/packages/kit/test/apps/basics/src/routes/encoded/%40[username]/+page.svelte b/packages/kit/test/apps/basics/src/routes/encoded/@[username]/+page.svelte
similarity index 100%
rename from packages/kit/test/apps/basics/src/routes/encoded/%40[username]/+page.svelte
rename to packages/kit/test/apps/basics/src/routes/encoded/@[username]/+page.svelte
diff --git a/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/+layout.svelte b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/+layout.svelte
new file mode 100644
index 000000000..47f4385e8
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/+layout.svelte
@@ -0,0 +1,16 @@
+<script>
+	import { page } from '$app/stores';
+</script>
+
+<h1>{decodeURIComponent($page.url.pathname.split('/').pop())}</h1>
+<slot />
+
+<a href="/encoded/escape-sequences/:-)">:-)</a>
+<a href="/encoded/escape-sequences/%23">#</a>
+<a href="/encoded/escape-sequences/%2F">/</a>
+<a href="/encoded/escape-sequences/%3f">?</a>
+<a href="/encoded/escape-sequences/苗">苗</a>
+<a href="/encoded/escape-sequences/<">&lt;</a>
+<a href="/encoded/escape-sequences/1<2">1&lt;2</a>
+<a href="/encoded/escape-sequences/🤪">🤪</a>
+<a href="/encoded/escape-sequences/%25">%</a>
diff --git a/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/+page.svelte b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/+page.svelte
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[u+82d7]/+page.svelte b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[u+82d7]/+page.svelte
new file mode 100644
index 000000000..2ca7e14ee
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[u+82d7]/+page.svelte
@@ -0,0 +1,1 @@
+苗
diff --git a/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[u+d83e][u+dd2a]/+page.svelte b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[u+d83e][u+dd2a]/+page.svelte
new file mode 100644
index 000000000..fe2055e2a
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[u+d83e][u+dd2a]/+page.svelte
@@ -0,0 +1,1 @@
+🤪
diff --git a/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+23]/+page.svelte b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+23]/+page.svelte
new file mode 100644
index 000000000..792d60054
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+23]/+page.svelte
@@ -0,0 +1,1 @@
+#
diff --git a/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+25]/+page.svelte b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+25]/+page.svelte
new file mode 100644
index 000000000..314d73bf4
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+25]/+page.svelte
@@ -0,0 +1,1 @@
+%
diff --git a/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+2f]/+page.svelte b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+2f]/+page.svelte
new file mode 100644
index 000000000..b498fd495
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+2f]/+page.svelte
@@ -0,0 +1,1 @@
+/
diff --git a/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+3a]-[x+29]/+page.svelte b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+3a]-[x+29]/+page.svelte
new file mode 100644
index 000000000..467119586
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+3a]-[x+29]/+page.svelte
@@ -0,0 +1,1 @@
+:-)
diff --git a/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+3c]/+page.svelte b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+3c]/+page.svelte
new file mode 100644
index 000000000..dd3d9a7f0
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+3c]/+page.svelte
@@ -0,0 +1,1 @@
+&lt;
diff --git a/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+3f]/+page.svelte b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+3f]/+page.svelte
new file mode 100644
index 000000000..a1e2647d2
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x+3f]/+page.svelte
@@ -0,0 +1,1 @@
+?
diff --git a/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x][x+3c][y]/+page.svelte b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x][x+3c][y]/+page.svelte
new file mode 100644
index 000000000..974d0d231
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/encoded/escape-sequences/[x][x+3c][y]/+page.svelte
@@ -0,0 +1,1 @@
+1&lt;2
diff --git a/packages/kit/test/apps/basics/src/routes/load/cors/server-only/+page.server.js b/packages/kit/test/apps/basics/src/routes/load/cors/server-only/+page.server.js
index 9fbd10aed..7a412012c 100644
--- a/packages/kit/test/apps/basics/src/routes/load/cors/server-only/+page.server.js
+++ b/packages/kit/test/apps/basics/src/routes/load/cors/server-only/+page.server.js
@@ -1,4 +1,4 @@
-/** @type {import('./$types').PageLoad} */
+/** @type {import('./$types').PageServerLoad} */
 export async function load({ fetch, url }) {
 	const res = await fetch(`http://localhost:${url.searchParams.get('port')}`);
 
diff --git a/packages/kit/test/apps/basics/src/routes/redirect/missing-status/b/+page.js b/packages/kit/test/apps/basics/src/routes/redirect/missing-status/b/+page.js
index 5aba1d550..c269fa152 100644
--- a/packages/kit/test/apps/basics/src/routes/redirect/missing-status/b/+page.js
+++ b/packages/kit/test/apps/basics/src/routes/redirect/missing-status/b/+page.js
@@ -1,5 +1,6 @@
 import { redirect } from '@sveltejs/kit';
 
 export function load() {
+	// @ts-ignore
 	throw redirect(555, './a');
 }
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index 5f9e5a2f7..46a003d51 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -323,17 +323,42 @@ test.describe('Encoded paths', () => {
 		});
 	});
 
-	test('allows %-encoded characters in directory names', async ({ page, clicknav }) => {
-		await page.goto('/encoded');
-		await clicknav('[href="/encoded/$SVLT"]');
-		expect(await page.textContent('h1')).toBe('$SVLT');
-	});
-
-	test('allows %-encoded characters in filenames', async ({ page, clicknav }) => {
+	test('allows non-ASCII character in parameterized route segment', async ({ page, clicknav }) => {
 		await page.goto('/encoded');
 		await clicknav('[href="/encoded/@svelte"]');
 		expect(await page.textContent('h1')).toBe('@svelte');
 	});
+
+	test('allows characters to be represented as escape sequences', async ({ page, clicknav }) => {
+		await page.goto('/encoded/escape-sequences');
+
+		await clicknav('[href="/encoded/escape-sequences/:-)"]');
+		expect(await page.textContent('h1')).toBe(':-)');
+
+		await clicknav('[href="/encoded/escape-sequences/%23"]');
+		expect(await page.textContent('h1')).toBe('#');
+
+		await clicknav('[href="/encoded/escape-sequences/%2F"]');
+		expect(await page.textContent('h1')).toBe('/');
+
+		await clicknav('[href="/encoded/escape-sequences/%3f"]');
+		expect(await page.textContent('h1')).toBe('?');
+
+		await clicknav('[href="/encoded/escape-sequences/%25"]');
+		expect(await page.textContent('h1')).toBe('%');
+
+		await clicknav('[href="/encoded/escape-sequences/<"]');
+		expect(await page.textContent('h1')).toBe('<');
+
+		await clicknav('[href="/encoded/escape-sequences/1<2"]');
+		expect(await page.textContent('h1')).toBe('1<2');
+
+		await clicknav('[href="/encoded/escape-sequences/苗"]');
+		expect(await page.textContent('h1')).toBe('苗');
+
+		await clicknav('[href="/encoded/escape-sequences/🤪"]');
+		expect(await page.textContent('h1')).toBe('🤪');
+	});
 });
 
 test.describe('$env', () => {