# How to make SvelteKit pages readable by AI agents

Serve the same page as HTML to browsers and as Markdown or JSON to anything that asks with an Accept header or a .md extension. vela enable content-negotiation wires sveltekit-negotiate into your hooks, and each page decides what its Markdown looks like.

Pattern: [Enable content negotiation](https://velastack.dev/patterns/enable-content-negotiation) · Docs: https://docs.velastack.dev/enable/content-negotiation

More of a site's readers are programs now: coding agents fetching your docs, assistants summarising a post, scripts checking a status page. They all get the HTML, then guess at where the content is in it. HTTP has had a better answer since the start: the client says what it can read in the `Accept` header, and the server picks a representation. `vela enable content-negotiation` sets that up so a page can answer `text/markdown` with clean Markdown and `application/json` with its data, from the same `load` that renders the HTML.

This page is served that way. Add `.md` to its URL, or fetch it with `Accept: text/markdown`, and you get the Markdown this tutorial was written in.

## Prerequisites

Any SvelteKit project. No backend needed, and it does not have to be one vela created. The output below is from a vela project. In a plain `npx sv create` project the example page lands at `src/routes/negotiate` because there is no `(public)` group, `<Negotiate />` goes into `src/routes/+layout.svelte`, and `src/hooks.server.ts` is created if the project has none.

## Run the command

```sh
$ vela enable content-negotiation
✔ Installed sveltekit-negotiate
✔ Created 4 files, modified 3
  src/lib/negotiate.ts
  src/routes/(public)/negotiate/+page.server.ts
  src/routes/(public)/negotiate/+page.svelte
  src/hooks.server.ts
  src/hooks.ts
  src/routes/(public)/root-layout.svelte
```

## What was generated

### The formats

One module declares which types the site can serve and the URL extension that stands in for each header:

**src/lib/negotiate.ts**

```ts
import { createNegotiation } from "sveltekit-negotiate";

export const { handle, reroute, negotiate, Negotiate } = createNegotiation({
  "text/markdown": { extension: ".md" },
  "application/json": { extension: ".json" },
});
```

### The hooks

The server hook reads the `Accept` header or the extension, lets the page render, and swaps the HTML response for the negotiated payload on the way out. It runs first in the sequence:

**src/hooks.server.ts**

```ts
import { env } from "$env/dynamic/private";
import { sequence } from "@sveltejs/kit/hooks";
import { handlePocketbase } from "@velastack/pocketbase";
// [!code highlight:1]
import { handle as handleNegotiate } from "$lib/negotiate";

export const handle = sequence(
  // [!code highlight:1]
  handleNegotiate,
  handlePocketbase({
    pocketbaseUrl: env.POCKETBASE_URL,
    superuserEmail: env.POCKETBASE_SUPERUSER_EMAIL,
    superuserPassword: env.POCKETBASE_SUPERUSER_PASSWORD,
  }),
);
```

The universal `reroute` makes `/about.md` hit the `/about` route:

**src/hooks.ts**

```ts
import { reroute as negotiateReroute } from "$lib/negotiate";

export const reroute = ({ url }) => negotiateReroute(url.pathname);
```

A `<Negotiate />` component is added to the public layout. It carries the payload out of the page render so the hook can pick it up.

### An example page

`/negotiate` shows the whole idea in a dozen lines. The `load` returns its data for the HTML as usual, and spreads in a Markdown version. The handler only runs when Markdown was asked for:

**src/routes/(public)/negotiate/+page.server.ts**

```ts
import { negotiate } from "$lib/negotiate";

export const load = async ({ locals }) => {
  const message = "Hello, content negotiation!";

  return {
    message,
    ...negotiate(locals, {
      "text/markdown": () => `# ${message}`,
    }),
  };
};
```

## Try it

```sh
$ vela dev
```

```sh
curl -H "Accept: text/markdown" http://localhost:5173/negotiate
# Hello, content negotiation!

curl http://localhost:5173/negotiate.md
# Hello, content negotiation!
```

A browser still gets the page. A page that has no handler for the requested type answers `406 Not Acceptable` rather than HTML, so a client never mistakes one for the other. Every HTML response on a negotiable URL also carries `Vary: accept`, so a shared cache does not hand HTML to the next client asking for Markdown.

## Make a real page negotiable

The [blog](https://velastack.dev/tutorials/sveltekit-markdown-blog) is the obvious candidate: posts are Markdown already. Its content module exposes the raw source of a post, so a server `load` next to the post route can hand it over:

**src/routes/(public)/blog/[slug]/+page.server.ts**

```ts
import { negotiate } from "$lib/negotiate";
import { getBlogPost, getBlogPostRaw } from "$lib/content";

export const load = async ({ params, locals }) => {
  const post = getBlogPost(params.slug);

  return {
    ...negotiate(locals, {
      "text/markdown": () => getBlogPostRaw(params.slug) ?? "",
      "application/json": () =>
        post && { slug: post.slug, title: post.title, tags: post.tags },
    }),
  };
};
```

Now `/blog/hello-world.md` is the post as written, and `/blog/hello-world.json` is its metadata. The HTML page is unchanged.

> **Tip:**
> Keep the Markdown handler cheap and the output honest: the frontmatter is usually worth a heading and a one-line description at the top, and root-relative links should become absolute, since the reader is not on your site.

## Going further

- Composes with [i18n](https://velastack.dev/tutorials/sveltekit-translate-your-site): the two reroute hooks chain, so `/es/blog/hola.md` resolves
- For write access from scripts, [API keys](https://velastack.dev/tutorials/sveltekit-api-keys) are the other half
- Undo with `vela disable content-negotiation`
- [Pattern page: Content Negotiation](https://velastack.dev/patterns/enable-content-negotiation) · [Docs: vela enable content-negotiation](https://docs.velastack.dev/enable/content-negotiation) · [sveltekit-negotiate on GitHub](https://github.com/nathancahill/sveltekit-negotiate)
