# Next.js

A Next.js site with the App Router. The widget's elements sit in a client component. The car types' facts, such as names and descriptions, render on the server with the rental's secret key, so search engines see them.

## 1. Keys and the script

Put the keys in `.env.local`. The browser key is public. The secret key is an `sk_live_` key of the rental's Website channel, from **Settings · API keys** in Mellow, with `fleet.read`. It stays on the server.

```bash
NEXT_PUBLIC_MELLOW_KEY=pk_live_…
MELLOW_KEY=sk_live_…
```

Load the script once, in the root layout. `next/script` loads it after the page hydrates, so the elements draw after React is done with the page.

```tsx
// app/layout.tsx
import Script from "next/script";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script type="module" src="https://mllw.is/widget/1/mellow.js" />
      </body>
    </html>
  );
}
```

For TypeScript, save the three [type files](/widget/start#types-for-react-and-vue) in `types/mellow/`, and leave `vue.d.ts` out of `tsconfig.json` when the site has no Vue.

## 2. The elements in a client component

```tsx
// app/book/booking.tsx
"use client";

export function Booking() {
  return (
    <mellow-booking
      rental="your-rental"
      auth={process.env.NEXT_PUBLIC_MELLOW_KEY!}
      onmellow-booked={(event) => console.log("Booked", event.detail.reference)}
    />
  );
}
```

`auth` holds the browser key. React keeps `key` for itself, so the widget reads its key from `auth`. `onmellow-booked` listens for the `mellow-booked` event: React 19 takes a prop that starts with `on` as a listener on a custom element. With the type files, `event.detail` has its type.

## 3. Facts on the server

The page is a server component. It reads the car types from the API with the secret key, and puts the client component below them.

```tsx
// app/book/page.tsx
import { Booking } from "./booking";

type CarType = { id: string; name: string; description: string | null };

async function carTypes(): Promise<CarType[]> {
  const res = await fetch("https://api.mellow.is/v1/rentals/your-rental/car-types?locale=en", {
    headers: { authorization: `Bearer ${process.env.MELLOW_KEY}` },
    next: { revalidate: 3600 },
  });
  if (!res.ok) return [];
  return (await res.json()).data;
}

export default async function BookPage() {
  const list = await carTypes();
  return (
    <main>
      <h1>Book a camper</h1>
      {list.map((c) => (
        <article key={c.id}>
          <h2>{c.name}</h2>
          {c.description && <p>{c.description}</p>}
        </article>
      ))}
      <Booking />
    </main>
  );
}
```

`description` is short Markdown. Draw only paragraphs, lists, bold, italic and links from it.

## One page for each step

To give each step its own address, put the client component in a layout that every booking page shares. It then stays in the page across the moves, and Next's router takes each move with no full page load:

```tsx
// app/book/layout.tsx
import { Booking } from "./booking";

export default function BookLayout({ children }: { children: React.ReactNode }) {
  return (
    <main>
      {children}
      <Booking />
    </main>
  );
}
```

Make a page for each path, such as `app/book/insurance/page.tsx`. A page may return `null`. Then add `paths` and the router to the client component:

```tsx
// app/book/booking.tsx
"use client";

import { useEffect, useRef } from "react";
import { usePathname, useRouter } from "next/navigation";

type Root = HTMLElement & { storeReady: Promise<{ locate(): void }> };

const PATHS =
  "car-type=/book insurance=/book/insurance driver=/book/details pay=/book/pay done=/book/thanks";

export function Booking() {
  const ref = useRef<Root>(null);
  const router = useRouter();
  const pathname = usePathname();

  // After the router moved, the store reads the new address.
  useEffect(() => {
    customElements
      .whenDefined("mellow-booking")
      .then(() => ref.current?.storeReady)
      .then((store) => store?.locate());
  }, [pathname]);

  return (
    <mellow-booking
      ref={ref}
      rental="your-rental"
      auth={process.env.NEXT_PUBLIC_MELLOW_KEY!}
      paths={PATHS}
      onmellow-step={(event) => {
        // The next step is on another page: move with Next's router, not a full page load.
        const next = new URL(event.detail.path, location.href);
        if (next.pathname === location.pathname) return;
        event.preventDefault();
        router.push(next.pathname + next.search);
      }}
    />
  );
}
```

[Pages and paths](/widget/pages) says how the steps share the pages.

**Tried:** on 2026-10-20, with Next.js 16.4 and React 19.3 on localhost, against a local Mellow with the test key and a local `sk_live_` key: a `<mellow-search>` on the home page, the facts from the server, the flow from the car types to "Test booking" on `/book/thanks` with every move by Next's router, the `onmellow-booked` listener, and `next build` with the type check.
