Repository: sveltejs/kit (js)

Issue:
Bring back support for content negotiation (allow +page.svelte & +server.ts to co-exist)
### Describe the problem

Previous versions of SvelteKit supported content negotiation where a route could serve HTML to browsers or JSON data if an `accept` header was used for a data request (as well as supporting data being posted to the same route).

Existing sites may already use this, it's been part of HTTP since forever. You might want to replace such a site with SvelteKit (or previously relied on SvelteKit supporting this) but changing the existing URL structure would be problematic and impactful (other systems using endpoints, pages already indexed and publicized etc...)

I think a router should be flexible enough to support existing URL structures, even if they are not the favoured default for the framework.

### Describe the proposed solution

Support the `accept` header in the routing and allow `application/json` requests to an endpoint at the same route. Pages should only be concerned with GET requests with no `accept` header (or one for html content).

### Alternatives considered

Adding a load function to `+page.server.ts` allows a GET request for data, but makes it difficult to control the response and would limit things to JSON (maybe you want CSV, PDF, or XML). It also appears limited in terms of what control you have over the response (http headers etc...) and still leaves you unable to use other HTTP methods.

Update: it appears that I should be treating `load` as GET and the other methods are supported. However they are inconsistent in terms of signature and what response is returned.

### Importance

i cannot use SvelteKit without it

### Additional Information

_No response_

Pull request:
[feat] allow +server.js files next to +page files
Closes #5896

Also added a related paragraph to "Form Actions" because I saw some people asking about this specifically.

Marking as draft because I don't know what to make of this TODO:
https://github.com/sveltejs/kit/blob/55493cb64f420e413b91154cef6ad54a03b96d1f/packages/kit/src/core/adapt/builder.js#L49


### 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
- [x] 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/cyan-pianos-bake.md b/.changeset/cyan-pianos-bake.md
new file mode 100644
index 000000000..7991f006d
--- /dev/null
+++ b/.changeset/cyan-pianos-bake.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/package': patch
+---
+
+[fix] don't strip `type="application/.."` tags
diff --git a/.changeset/lazy-mice-remain.md b/.changeset/lazy-mice-remain.md
new file mode 100644
index 000000000..519d0d7fd
--- /dev/null
+++ b/.changeset/lazy-mice-remain.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[feat] allow +server.js files next to +page files
diff --git a/.changeset/pre.json b/.changeset/pre.json
index 1dfbec7e7..5d319f9d5 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -210,6 +210,7 @@
     "cyan-knives-mix",
     "cyan-numbers-change",
     "cyan-parents-enjoy",
+    "cyan-pianos-bake",
     "cyan-radios-attend",
     "cyan-roses-act",
     "cyan-schools-kiss",
@@ -271,6 +272,7 @@
     "empty-worms-appear",
     "fair-pets-bake",
     "fair-rivers-repair",
+    "fair-tables-peel",
     "famous-birds-boil",
     "famous-birds-cheer",
     "famous-boxes-tap",
diff --git a/documentation/docs/03-routing.md b/documentation/docs/03-routing.md
index ac5598dd7..3ca29c382 100644
--- a/documentation/docs/03-routing.md
+++ b/documentation/docs/03-routing.md
@@ -267,6 +267,57 @@ The first argument to `Response` can be a [`ReadableStream`](https://developer.m
 
 You can use the `error`, `redirect` and `json` methods from `@sveltejs/kit` for convenience (but you don't have to). Note that `throw error(..)` only returns a plain text error response.
 
+#### Receiving data
+
+By exporting `POST`/`PUT`/`PATCH`/`DELETE` handlers, `+server.js` files can be used to create a complete API:
+
+```svelte
+/// file: src/routes/add/+page.svelte
+<script>
+	let a = 0;
+	let b = 0;
+	let total = 0;
+
+	async function add() {
+		const response = await fetch('/api/add', {
+			method: 'POST',
+			body: JSON.stringify({ a, b }),
+			headers: {
+				'content-type': 'application/json'
+			}
+		});
+
+		total = await response.json();
+	}
+</script>
+
+<input type="number" bind:value={a}> +
+<input type="number" bind:value={b}> =
+{total}
+
+<button on:click={add}>Calculate</button>
+```
+
+```js
+/// file: src/routes/api/add/+server.js
+import { json } from '@sveltejs/kit';
+
+/** @type {import('./$types').RequestHandler} */
+export async function POST({ request }) {
+	const { a, b } = await request.json();
+	return json(a + b);
+}
+```
+
+> In general, [form actions](/docs/form-actions) are a better way to submit data from the browser to the server.
+
+#### Content negotiation
+
+`+server.js` files can be placed in the same directory as `+page` files, allowing the same route to be either a page or an API endpoint. To determine which, SvelteKit applies the following rules:
+
+- `PUT`/`PATCH`/`DELETE` requests are always handled by `+server.js` since they do not apply to pages
+- `GET`/`POST` requests are treated as page requests if the `accept` header prioritises `text/html` (in other words, it's a browser page request), else they are handled by `+server.js`
+
 ### $types
 
 Throughout the examples above, we've been importing types from a `$types.d.ts` file. This is a file SvelteKit creates for you in a hidden directory if you're using TypeScript (or JavaScript with JSDoc type annotations) to give you type safety when working with your root files.
diff --git a/documentation/docs/06-form-actions.md b/documentation/docs/06-form-actions.md
index fcf1300a8..d5fe40ad9 100644
--- a/documentation/docs/06-form-actions.md
+++ b/documentation/docs/06-form-actions.md
@@ -351,3 +351,7 @@ We can also implement progressive enhancement ourselves, without `use:enhance`,
 	<!-- content -->
 </form>
 ```
+
+### Alternatives
+
+Form actions are the preferred way to send data to the server, since they can be progressively enhanced, but you can also use [`+server.js`](/docs/routing#server) files to expose (for example) a JSON API.
\ No newline at end of file
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index 7b8e26355..af144415f 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/kit
 
+## 1.0.0-next.486
+
+### Patch Changes
+
+- [fix] handle jsdoc without tags while generating proxy types ([#6884](https://github.com/sveltejs/kit/pull/6884))
+
 ## 1.0.0-next.485
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index 8fdaa7aa4..1ac67552d 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.0.0-next.485",
+	"version": "1.0.0-next.486",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js
index b79fb68d7..f8dcc36d1 100644
--- a/packages/kit/src/core/adapt/builder.js
+++ b/packages/kit/src/core/adapt/builder.js
@@ -46,7 +46,6 @@ export function create_builder({ config, build_data, routes, prerendered, log })
 
 				return {
 					id: route.id,
-					type: route.page ? 'page' : 'endpoint', // TODO change this if support pages+endpoints
 					segments: route.id.split('/').map((segment) => ({
 						dynamic: segment.includes('['),
 						rest: segment.includes('[...'),
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 c386bf1ed..563c391b0 100644
--- a/packages/kit/src/core/sync/create_manifest_data/index.js
+++ b/packages/kit/src/core/sync/create_manifest_data/index.js
@@ -272,11 +272,6 @@ function create_routes_and_nodes(cwd, config, fallback) {
 		route_map.forEach((route) => {
 			if (!route.leaf) return;
 
-			if (route.leaf && route.endpoint) {
-				// TODO possibly relax this https://github.com/sveltejs/kit/issues/5896
-				throw new Error(`${route.endpoint.file} cannot share a directory with other route files`);
-			}
-
 			route.page = {
 				layouts: [],
 				errors: [],
diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js
index 5e3aba084..f80ed4417 100644
--- a/packages/kit/src/runtime/server/endpoint.js
+++ b/packages/kit/src/runtime/server/endpoint.js
@@ -1,4 +1,5 @@
 import { json } from '../../exports/index.js';
+import { negotiate } from '../../utils/http.js';
 import { Redirect, ValidationError } from '../control.js';
 import { check_method_names, method_not_allowed } from './utils.js';
 
@@ -64,3 +65,19 @@ export async function render_endpoint(event, mod, state) {
 		throw error;
 	}
 }
+
+/**
+ * @param {import('types').RequestEvent} event
+ */
+export function is_endpoint_request(event) {
+	const { method } = event.request;
+
+	if (method === 'PUT' || method === 'PATCH' || method === 'DELETE') {
+		// These methods exist exclusively for endpoints
+		return true;
+	}
+
+	// GET/POST requests may be for endpoints or pages. We prefer endpoints if this isn't a text/html request
+	const accept = event.request.headers.get('accept') ?? '*/*';
+	return negotiate(accept, ['*', 'text/html']) !== 'text/html';
+}
diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js
index cff90f3c0..9070608a0 100644
--- a/packages/kit/src/runtime/server/index.js
+++ b/packages/kit/src/runtime/server/index.js
@@ -1,4 +1,4 @@
-import { render_endpoint } from './endpoint.js';
+import { is_endpoint_request, render_endpoint } from './endpoint.js';
 import { render_page } from './page/index.js';
 import { render_response } from './page/render.js';
 import { respond_with_error } from './page/respond_with_error.js';
@@ -226,10 +226,10 @@ export async function respond(request, options, state) {
 
 				if (is_data_request) {
 					response = await render_data(event, route, options, state);
+				} else if (route.endpoint && (!route.page || is_endpoint_request(event))) {
+					response = await render_endpoint(event, await route.endpoint(), state);
 				} else if (route.page) {
 					response = await render_page(event, route, route.page, options, state, resolve_opts);
-				} else if (route.endpoint) {
-					response = await render_endpoint(event, await route.endpoint(), state);
 				} else {
 					// a route will always have a page or an endpoint, but TypeScript
 					// doesn't know that
diff --git a/packages/kit/types/private.d.ts b/packages/kit/types/private.d.ts
index 24b587ddc..18a2c1091 100644
--- a/packages/kit/types/private.d.ts
+++ b/packages/kit/types/private.d.ts
@@ -195,7 +195,6 @@ export interface RequestOptions {
 
 export interface RouteDefinition {
 	id: string;
-	type: 'page' | 'endpoint';
 	pattern: RegExp;
 	segments: RouteSegment[];
 	methods: HttpMethod[];
diff --git a/packages/package/CHANGELOG.md b/packages/package/CHANGELOG.md
index dc82b369c..0fbb8b088 100644
--- a/packages/package/CHANGELOG.md
+++ b/packages/package/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/package
 
+## 1.0.0-next.4
+
+### Patch Changes
+
+- [fix] don't strip `type="application/.."` tags ([#6887](https://github.com/sveltejs/kit/pull/6887))
+
 ## 1.0.0-next.3
 
 ### Patch Changes
diff --git a/packages/package/package.json b/packages/package/package.json
index 357a0da4a..b5bf86863 100644
--- a/packages/package/package.json
+++ b/packages/package/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/package",
-	"version": "1.0.0-next.3",
+	"version": "1.0.0-next.4",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/package/src/utils.js b/packages/package/src/utils.js
index 97c3b03db..0b263857b 100644
--- a/packages/package/src/utils.js
+++ b/packages/package/src/utils.js
@@ -49,7 +49,11 @@ export function resolve_lib_alias(file, content, config) {
  */
 export function strip_lang_tags(content) {
 	return content
-		.replace(/(<!--[^]*?-->)|(<script[^>]*?)\s(?:type|lang)=(["']).*?\3/g, '$1$2')
+		.replace(
+			/(<!--[^]*?-->)|(<script[^>]*?)\s(?:type|lang)=(["'])(.*?)\3/g,
+			// things like application/ld+json should be kept as-is. Preprocessed languages are "ts" etc
+			(match, s1, s2, _, s4) => (s4.startsWith('application/') ? match : (s1 ?? '') + (s2 ?? ''))
+		)
 		.replace(/(<!--[^]*?-->)|(<style[^>]*?)\s(?:type|lang)=(["']).*?\3/g, '$1$2');
 }
 


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+page.svelte
new file mode 100644
index 000000000..b88ab6af7
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+page.svelte
@@ -0,0 +1,18 @@
+<script>
+	let result;
+
+	/** @param {string} method */
+	async function request(method) {
+		result = 'loading';
+		const response = await fetch('/routing/endpoint-next-to-page', {method});
+		result = await response.text();
+	}
+</script>
+
+<p>Hi</p>
+<button on:click={() => request('GET')}>GET</button>
+<button on:click={() => request('PUT')}>PUT</button>
+<button on:click={() => request('PATCH')}>PATCH</button>
+<button on:click={() => request('POST')}>POST</button>
+<button on:click={() => request('DELETE')}>DELETE</button>
+<pre>{result}</pre>
diff --git a/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+server.js b/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+server.js
new file mode 100644
index 000000000..007c90f0d
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+server.js
@@ -0,0 +1,24 @@
+/** @type {import('./$types').RequestHandler} */
+export function GET() {
+	return new Response('GET');
+}
+
+/** @type {import('./$types').RequestHandler} */
+export function PUT() {
+	return new Response('PUT');
+}
+
+/** @type {import('./$types').RequestHandler} */
+export function PATCH() {
+	return new Response('PATCH');
+}
+
+/** @type {import('./$types').RequestHandler} */
+export function POST() {
+	return new Response('POST');
+}
+
+/** @type {import('./$types').RequestHandler} */
+export function DELETE() {
+	return new Response('DELETE');
+}
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index cf12fdc98..42dedf5b7 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -8,7 +8,7 @@ test.skip(({ javaScriptEnabled }) => !javaScriptEnabled);
 test.describe.configure({ mode: 'parallel' });
 
 test.describe('beforeNavigate', () => {
-	test('prevents navigation triggered by link click', async ({ clicknav, page, baseURL }) => {
+	test('prevents navigation triggered by link click', async ({ page, baseURL }) => {
 		await page.goto('/before-navigate/prevent-navigation');
 
 		await page.click('[href="/before-navigate/a"]');
@@ -807,3 +807,23 @@ test.describe('data-sveltekit attributes', () => {
 		expect(await page.evaluate(() => window.scrollY)).toBe(0);
 	});
 });
+
+test('+server.js next to +page.svelte works', async ({ page }) => {
+	await page.goto('/routing/endpoint-next-to-page');
+	expect(await page.textContent('p')).toBe('Hi');
+
+	await page.click('button:has-text("GET")');
+	await expect(page.locator('pre')).toHaveText('GET');
+
+	await page.click('button:has-text("PUT")');
+	await expect(page.locator('pre')).toHaveText('PUT');
+
+	await page.click('button:has-text("PATCH")');
+	await expect(page.locator('pre')).toHaveText('PATCH');
+
+	await page.click('button:has-text("POST")');
+	await expect(page.locator('pre')).toHaveText('POST');
+
+	await page.click('button:has-text("DELETE")');
+	await expect(page.locator('pre')).toHaveText('DELETE');
+});
diff --git a/packages/package/test/fixtures/typescript/expected/Plain.svelte b/packages/package/test/fixtures/typescript/expected/Plain.svelte
index b49daed30..29329add3 100644
--- a/packages/package/test/fixtures/typescript/expected/Plain.svelte
+++ b/packages/package/test/fixtures/typescript/expected/Plain.svelte
@@ -2,3 +2,7 @@
 	/** @type {import('./foo').Foo} */
 	export let foo;
 </script>
+
+<svelte:head>
+  <script type="application/ld+json">{JSON.stringify(jsonLd)}</script>
+</svelte:head>
diff --git a/packages/package/test/fixtures/typescript/src/lib/Plain.svelte b/packages/package/test/fixtures/typescript/src/lib/Plain.svelte
index b49daed30..29329add3 100644
--- a/packages/package/test/fixtures/typescript/src/lib/Plain.svelte
+++ b/packages/package/test/fixtures/typescript/src/lib/Plain.svelte
@@ -2,3 +2,7 @@
 	/** @type {import('./foo').Foo} */
 	export let foo;
 </script>
+
+<svelte:head>
+  <script type="application/ld+json">{JSON.stringify(jsonLd)}</script>
+</svelte:head>
diff --git a/packages/package/test/fixtures/typescript/svelte.config.js b/packages/package/test/fixtures/typescript/svelte.config.js
index 6bd253bdf..8e74287e5 100644
--- a/packages/package/test/fixtures/typescript/svelte.config.js
+++ b/packages/package/test/fixtures/typescript/svelte.config.js
@@ -1,5 +1,7 @@
 import preprocess from 'svelte-preprocess';
 
 export default {
-	preprocess: preprocess()
+	preprocess: preprocess({
+		preserve: ['ld+json']
+	})
 };