Repository: sveltejs/kit (js)

Issue:
`sveltekit:prefetch` seems to be invalid HTML
### Describe the problem

Hey, I just validated my SvelteKit website with https://validator.w3.org/nu/ and it heavily complained about `sveltekit:prefetch` not being valid HTML.

Is this a problem? Maybe if some service wanted to rank the quality of some website, they would use an HTML validator and adjust the ranking accordingly?

### Describe the proposed solution

To avoid that, maybe the Svelte compiler could automatically turn such things into a valid HTML attribute like `data-sveltekit="prefetch"` from where the router can pick it up?

In my imagination this should be fairly easy to implement and would result in valid HTML.

### Alternatives considered

_No response_

### Importance

nice to have

### Additional Information

Thank you very much for building Svelte!

Pull request:
feature: Replace `sveltekit:*` with valid HTML attributes like `data-sveltekit-*`
I'm opening this early draft PR to track our progress to fix #3186

### Goals
Replace `sveltekit:*` attributes (where `*` is `prefetch`, `noscroll` or `reload`) with valid HTML attributes `data-sveltekit-*`.

- Hopefully, this approach will fix #3186 without a breaking change for app developers.
- Extend the existing preprocessing happening in `vite-plugin-svelte`, which extends any custom `config.preprocess` defined by app developers.
- Improve type definition for `config.preprocess` to improve DX in `svelte.config.js` files.

### Remaining TODOs:

- [ ] **Critical:** Improve attribute replacement, either with improved regexes or by using a better kind of preprocessor from `svelte-preprocess`. Help appreciated in this area.
- [ ] Fix 1 breaking test `Scrolling › scroll is restored after hitting the back button`
- [ ] Add changeset
- [ ] Cleanup example project
- [ ] Add tests t define expected behavior. Need to look into this more.
- [ ] Ensure this works well with `svelte-kit package` when building components etc.

### 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
- [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [ ] 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/neat-pots-flash.md b/.changeset/neat-pots-flash.md
new file mode 100644
index 000000000..80fa9c864
--- /dev/null
+++ b/.changeset/neat-pots-flash.md
@@ -0,0 +1,6 @@
+---
+"create-svelte": patch
+"@sveltejs/kit": patch
+---
+
+[breaking] Replace `sveltekit:*` with valid HTML attributes like `data-sveltekit-*`
diff --git a/documentation/docs/09-a-options.md b/documentation/docs/09-a-options.md
index ac5f6206a..c5184f779 100644
--- a/documentation/docs/09-a-options.md
+++ b/documentation/docs/09-a-options.md
@@ -2,16 +2,16 @@
 title: Anchor options
 ---
 
-### sveltekit:prefetch
+### data-sveltekit-prefetch
 
 SvelteKit uses code splitting to break your app into small chunks (one per route), ensuring fast startup times.
 
 For _dynamic_ routes, such as our `src/routes/blog/[slug]/+page.svelte` example, that's not enough. In order to render the blog post, we need to fetch the data for it, and we can't do that until we know what `slug` is. In the worst case, that could cause lag as the browser waits for the data to come back from the server.
 
-We can mitigate that by _prefetching_ the data. Adding a `sveltekit:prefetch` attribute to a link...
+We can mitigate that by _prefetching_ the data. Adding a `data-sveltekit-prefetch` attribute to a link...
 
 ```html
-<a sveltekit:prefetch href="blog/what-is-sveltekit">What is SvelteKit?</a>
+<a data-sveltekit-prefetch href="blog/what-is-sveltekit">What is SvelteKit?</a>
 ```
 
 ...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.
@@ -20,28 +20,28 @@ Note that prefetching will not work if the [`router`](/docs/page-options#router)
 
 You can also programmatically invoke `prefetch` from `$app/navigation`.
 
-### sveltekit:reload
+### data-sveltekit-reload
 
 By default, the SvelteKit runtime intercepts clicks on `<a>` elements and bypasses the normal browser navigation for relative (same-origin) URLs that match one of your page routes. We sometimes need to tell SvelteKit that certain links need to be handled by normal browser navigation. Examples of this might be linking to another page on your domain that's not part of your SvelteKit app or linking to an endpoint.
 
-Adding a `sveltekit:reload` attribute to a link...
+Adding a `data-sveltekit-reload` attribute to a link...
 
 ```html
-<a sveltekit:reload href="path">Path</a>
+<a data-sveltekit-reload href="path">Path</a>
 ```
 
 ...will cause browser to navigate via a full page reload when the link is clicked.
 
 Links with a `rel="external"` attribute will receive the same treatment. In addition, they will be ignored during [prerendering](https://kit.svelte.dev/docs/page-options#prerender).
 
-### sveltekit:noscroll
+### data-sveltekit-noscroll
 
 When navigating to internal links, SvelteKit mirrors the browser's default navigation behaviour: it will change the scroll position to 0,0 so that the user is at the very top left of the page (unless the link includes a `#hash`, in which case it will scroll to the element with a matching ID).
 
-In certain cases, you may wish to disable this behaviour. Adding a `sveltekit:noscroll` attribute to a link...
+In certain cases, you may wish to disable this behaviour. Adding a `data-sveltekit-noscroll` attribute to a link...
 
 ```html
-<a href="path" sveltekit:noscroll>Path</a>
+<a href="path" data-sveltekit-noscroll>Path</a>
 ```
 
 ...will prevent scrolling after the link is clicked.
diff --git a/documentation/docs/80-migrating.md b/documentation/docs/80-migrating.md
index 1ef080470..69f7bdeae 100644
--- a/documentation/docs/80-migrating.md
+++ b/documentation/docs/80-migrating.md
@@ -133,8 +133,8 @@ This caused problems and is no longer the case in SvelteKit. Instead, relative U
 
 #### &lt;a&gt; attributes
 
-- `sapper:prefetch` is now `sveltekit:prefetch`
-- `sapper:noscroll` is now `sveltekit:noscroll`
+- `sapper:prefetch` is now `data-sveltekit-prefetch`
+- `sapper:noscroll` is now `data-sveltekit-noscroll`
 
 ### Endpoints
 
diff --git a/packages/create-svelte/templates/default/src/lib/header/Header.svelte b/packages/create-svelte/templates/default/src/lib/header/Header.svelte
index 9d3120f3e..a048e0ecc 100644
--- a/packages/create-svelte/templates/default/src/lib/header/Header.svelte
+++ b/packages/create-svelte/templates/default/src/lib/header/Header.svelte
@@ -15,12 +15,14 @@
 			<path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" />
 		</svg>
 		<ul>
-			<li class:active={$page.url.pathname === '/'}><a sveltekit:prefetch href="/">Home</a></li>
+			<li class:active={$page.url.pathname === '/'}>
+				<a data-sveltekit-prefetch href="/">Home</a>
+			</li>
 			<li class:active={$page.url.pathname === '/about'}>
-				<a sveltekit:prefetch href="/about">About</a>
+				<a data-sveltekit-prefetch href="/about">About</a>
 			</li>
 			<li class:active={$page.url.pathname === '/todos'}>
-				<a sveltekit:prefetch href="/todos">Todos</a>
+				<a data-sveltekit-prefetch href="/todos">Todos</a>
 			</li>
 		</ul>
 		<svg viewBox="0 0 2 3" aria-hidden="true">
diff --git a/packages/kit/src/core/prerender/fixtures/basic-href/input.html b/packages/kit/src/core/prerender/fixtures/basic-href/input.html
index ec2f5dc57..a294e070d 100644
--- a/packages/kit/src/core/prerender/fixtures/basic-href/input.html
+++ b/packages/kit/src/core/prerender/fixtures/basic-href/input.html
@@ -7,8 +7,8 @@
 	<body>
 		<a href="https://external.com">https://external.com</a>
 		<a href="/wheee">/wheee</a>
-		<a sveltekit:prefetch href="/wheee2">/wheee</a>
-		<a href="/wheee3" sveltekit:prefetch>/wheee</a>
+		<a data-sveltekit-prefetch href="/wheee2">/wheee</a>
+		<a href="/wheee3" data-sveltekit-prefetch>/wheee</a>
 		<a href="/wheee4">/wheee</a>
 	</body>
 </html>
diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js
index 7f56d9e46..8394d6427 100644
--- a/packages/kit/src/exports/vite/index.js
+++ b/packages/kit/src/exports/vite/index.js
@@ -195,6 +195,7 @@ function kit() {
 		async config(config, config_env) {
 			vite_config_env = config_env;
 			svelte_config = await load_config();
+
 			env = get_env(svelte_config.kit.env, vite_config_env.mode);
 
 			// The config is created in build_server for SSR mode and passed inline
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index 64c470be6..7c78fb29f 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -44,6 +44,23 @@ function update_scroll_positions(index) {
 	scroll_positions[index] = scroll_state();
 }
 
+/** @type {Record<string, true>} */
+let warned_about_attributes = {};
+
+function check_for_removed_attributes() {
+	const attrs = ['prefetch', 'noscroll', 'reload'];
+	for (const attr of attrs) {
+		if (document.querySelector(`[sveltekit\\:${attr}]`)) {
+			if (!warned_about_attributes[attr]) {
+				warned_about_attributes[attr] = true;
+				console.error(
+					`The sveltekit:${attr} attribute has been replaced with data-sveltekit-${attr}`
+				);
+			}
+		}
+	}
+}
+
 /**
  * @param {{
  *   target: Element;
@@ -271,6 +288,8 @@ export function create_client({ target, base, trailing_slash }) {
 				};
 				root.$set(navigation_result.props);
 				tick().then(() => (console.warn = warn));
+
+				check_for_removed_attributes();
 			} else {
 				root.$set(navigation_result.props);
 			}
@@ -369,6 +388,8 @@ export function create_client({ target, base, trailing_slash }) {
 				hydrate: true
 			});
 			console.warn = warn;
+
+			check_for_removed_attributes();
 		} else {
 			root = new Root({
 				target,
@@ -1138,7 +1159,7 @@ export function create_client({ target, base, trailing_slash }) {
 			/** @param {Event} event */
 			const trigger_prefetch = (event) => {
 				const a = find_anchor(event);
-				if (a && a.href && a.hasAttribute('sveltekit:prefetch')) {
+				if (a && a.href && a.hasAttribute('data-sveltekit-prefetch')) {
 					prefetch(get_href(a));
 				}
 			};
@@ -1195,7 +1216,7 @@ export function create_client({ target, base, trailing_slash }) {
 				if (
 					a.hasAttribute('download') ||
 					rel.includes('external') ||
-					a.hasAttribute('sveltekit:reload')
+					a.hasAttribute('data-sveltekit-reload')
 				) {
 					return;
 				}
@@ -1222,7 +1243,7 @@ export function create_client({ target, base, trailing_slash }) {
 
 				navigate({
 					url,
-					scroll: a.hasAttribute('sveltekit:noscroll') ? scroll_state() : null,
+					scroll: a.hasAttribute('data-sveltekit-noscroll') ? scroll_state() : null,
 					keepfocus: false,
 					redirect_chain: [],
 					details: {
diff --git a/packages/kit/types/ambient.d.ts b/packages/kit/types/ambient.d.ts
index 406ea05a0..68b4da04c 100644
--- a/packages/kit/types/ambient.d.ts
+++ b/packages/kit/types/ambient.d.ts
@@ -122,7 +122,7 @@ declare module '$app/navigation' {
 	 *  1. ensuring that the code for the page is loaded, and
 	 *  2. calling the page's load function with the appropriate options.
 	 *
-	 * This is the same behaviour that SvelteKit triggers when the user taps or mouses over an `<a>` element with `sveltekit:prefetch`.
+	 * This is the same behaviour that SvelteKit triggers when the user taps or mouses over an `<a>` element with `data-sveltekit-prefetch`.
 	 * If the next navigation is to `href`, the values returned from load will be used, making navigation instantaneous.
 	 * Returns a Promise that resolves when the prefetch is complete.
 	 *
diff --git a/sites/kit.svelte.dev/src/lib/docs/Contents.svelte b/sites/kit.svelte.dev/src/lib/docs/Contents.svelte
index 1b538565f..dbed3dfcf 100644
--- a/sites/kit.svelte.dev/src/lib/docs/Contents.svelte
+++ b/sites/kit.svelte.dev/src/lib/docs/Contents.svelte
@@ -82,7 +82,7 @@
 		{#each contents as section}
 			<li>
 				<a
-					sveltekit:prefetch
+					data-sveltekit-prefetch
 					class="section"
 					class:active={section.path === path}
 					href={section.path}
@@ -95,7 +95,7 @@
 					{#each section.sections as subsection}
 						<li>
 							<a
-								sveltekit:prefetch
+								data-sveltekit-prefetch
 								class="subsection"
 								class:active={subsection.path === path}
 								href={subsection.path}
@@ -109,7 +109,7 @@
 									{#each subsection.sections as subsection}
 										<li>
 											<a
-												sveltekit:prefetch
+												data-sveltekit-prefetch
 												class="nested subsection"
 												class:active={subsection.path === path}
 												href={subsection.path}
diff --git a/sites/kit.svelte.dev/src/lib/search/SearchResultList.svelte b/sites/kit.svelte.dev/src/lib/search/SearchResultList.svelte
index 94530a59b..70b5bc611 100644
--- a/sites/kit.svelte.dev/src/lib/search/SearchResultList.svelte
+++ b/sites/kit.svelte.dev/src/lib/search/SearchResultList.svelte
@@ -37,7 +37,7 @@
 	{#each results as result, i}
 		<li>
 			<a
-				sveltekit:prefetch
+				data-sveltekit-prefetch
 				href={result.href}
 				on:click={() => dispatch('select', { href: result.href })}
 				data-has-node={result.node ? true : undefined}
diff --git a/sites/kit.svelte.dev/src/routes/+page.svelte b/sites/kit.svelte.dev/src/routes/+page.svelte
index 3aa00a046..a18aa397b 100644
--- a/sites/kit.svelte.dev/src/routes/+page.svelte
+++ b/sites/kit.svelte.dev/src/routes/+page.svelte
@@ -40,7 +40,7 @@
 				of an SPA
 			</p>
 
-			<a sveltekit:prefetch href="/docs" class="cta">read the docs</a>
+			<a data-sveltekit-prefetch href="/docs" class="cta">read the docs</a>
 		</div>
 
 		<div slot="three">
@@ -50,7 +50,7 @@
 				support and more
 			</p>
 
-			<a sveltekit:prefetch href="/docs" class="cta">read the docs</a>
+			<a data-sveltekit-prefetch href="/docs" class="cta">read the docs</a>
 		</div>
 
 		<div class="description" slot="what">
@@ -77,7 +77,7 @@ cd my-app
 npm install
 npm run dev -- --open</code
 				></pre>
-			<a sveltekit:prefetch href="/docs" class="cta">get started</a>
+			<a data-sveltekit-prefetch href="/docs" class="cta">get started</a>
 		</div>
 	</Blurb>
 </div>


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/routing/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/+page.svelte
index 81b4fef41..61f62ec6f 100644
--- a/packages/kit/test/apps/basics/src/routes/routing/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/routing/+page.svelte
@@ -9,6 +9,6 @@
 <a href="http://localhost:{$page.url.searchParams.get('port')}">elsewhere</a>
 <a href="/static.json">static.json</a>
 
-<a href="/routing/b" sveltekit:reload>b</a>
+<a href="/routing/b" data-sveltekit-reload>b</a>
 
 <div class="hydrate-test" />
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index c659740d2..d85d6ab35 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -1427,7 +1427,7 @@ test.describe('Routing', () => {
 		expect(await page.textContent('body')).toBe('ok');
 	});
 
-	test('does not attempt client-side navigation to links with sveltekit:reload', async ({
+	test('does not attempt client-side navigation to links with data-sveltekit-reload', async ({
 		baseURL,
 		page
 	}) => {
diff --git a/packages/kit/test/apps/options/source/pages/prefetching/+page.svelte b/packages/kit/test/apps/options/source/pages/prefetching/+page.svelte
index 07b287bef..4d55ffcdf 100644
--- a/packages/kit/test/apps/options/source/pages/prefetching/+page.svelte
+++ b/packages/kit/test/apps/options/source/pages/prefetching/+page.svelte
@@ -1,1 +1,1 @@
-<a sveltekit:prefetch href="/path-base/prefetching/prefetched">click me</a>
+<a data-sveltekit-prefetch href="/path-base/prefetching/prefetched">click me</a>