# Next.js

> The App Router with the page rendered on the server and the answer drawn in a client component.

```sh
npm install @morphscreen/web
```

The core runs in the browser, so the answer goes in a client component. The page around it stays a server component.

## Serve the core

Copy `morphscreen.wasm` into `public/` before each build, so it's served at `/morphscreen.wasm`:

```js
// Copies the core out of the package into public/, where the app serves it
// and loadCore("/morphscreen.wasm") fetches it. Run before each build.
import { copyFileSync, mkdirSync } from "node:fs";
import { createRequire } from "node:module";

const require = createRequire(import.meta.url);
mkdirSync("public", { recursive: true });
copyFileSync(require.resolve("@morphscreen/web/morphscreen.wasm"), "public/morphscreen.wasm");
```

```json
{ "scripts": { "build": "node copy-wasm.mjs && next build" } }
```

## The stylesheet

```tsx
import "@morphscreen/web/morphscreen.css";

export const metadata = { title: "MorphScreen with Next.js" };

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}
```

## The answer, in a client component

```tsx
"use client";

import { useEffect, useState } from "react";
import { loadCore, type Core } from "@morphscreen/web";
import { Answer } from "@morphscreen/web/react";
import { SAMPLE } from "./sample";

export function LiveAnswer() {
  const [core, setCore] = useState<Core | null>(null);
  const [text, setText] = useState("");
  useEffect(() => {
    loadCore("/morphscreen.wasm").then(setCore);
  }, []);
  // Your server's stream goes here; this grows a saved answer piece by piece.
  useEffect(() => {
    if (!core) return;
    let n = 0;
    const t = setInterval(() => {
      n += 24;
      setText(SAMPLE.slice(0, n));
      if (n >= SAMPLE.length) clearInterval(t);
    }, 10);
    return () => clearInterval(t);
  }, [core]);
  const done = text.length >= SAMPLE.length;
  useEffect(() => {
    if (done) document.body.dataset.done = "yes";
  }, [done]);
  return core ? <Answer core={core} text={text} done={done} /> : null;
}
```

```tsx
import { LiveAnswer } from "./answer";

// A server component: the page renders on the server, the answer in the browser.
export default function Page() {
  return (
    <main>
      <LiveAnswer />
    </main>
  );
}
```

## Your server route

Your `/api/agent` route can be a Route Handler doing what [the Quick start's server](/docs/quickstart) does: add `X-MorphScreen-Key`, set the model, and return MorphScreen's response body as the response.

```ts
// The page's AG-UI runs go on to MorphScreen with your key, and the events
// stream straight back. The key never reaches the browser, and the server
// picks the model.
export async function POST(req: Request) {
  const run = await req.json();
  run.forwardedProps = { ...run.forwardedProps, model: "deepseek/deepseek-v4.1-flash" };
  const upstream = await fetch("https://api.morphscreen.com/v1/ui", {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-MorphScreen-Key": process.env.MORPHSCREEN_KEY ?? "" },
    body: JSON.stringify(run),
  });
  return new Response(upstream.body, { status: upstream.status, headers: { "Content-Type": upstream.headers.get("content-type") ?? "text/event-stream" } });
}
```
