# Install on Next.js

> Install Kismet Telemetry in a Next.js app with @kismet-tech/telemetry-next, three files, and verify with six checks.


`@kismet-tech/telemetry-next` wraps the [core](https://developers.kismet.travel/telemetry.md) for the App Router: a middleware that does the whole [contract](https://developers.kismet.travel/telemetry/contract.md) on every page request, a seed component for the root layout, and the checkout call. Edge or Node runtime; Next 14.2 or later.


**Contract 1.0 limit:** `externalListingId` can be emitted, but ingest-side resolution and generic conversions are Contract 1.1 additions. For property-level attribution today, arrange a registered serving-URL mapping or use a known Kismet property slug. Confirm backend support before relying on the 1.1 fields.

## Before you start

Request from Kismet during onboarding: the collection slug and the collection's tracking key. Put them in the environment, never in a page:

```dotenv
KISMET_COLLECTION_SLUG=your-collection
KISMET_TRACKING_KEY=ctk_...
```

Confirm your production and staging hostnames are on the collection's authorized domains. The browser tracker's origin is its credential; an unregistered hostname means client-side events are rejected once enforcement is on.

```sh
npm install @kismet-tech/telemetry@1.1.1 @kismet-tech/telemetry-next@1.1.1
```

## 1. The middleware

```ts
// middleware.ts
import { createKismetMiddleware, KISMET_MATCHER, consentFromCookie } from '@kismet-tech/telemetry-next';

export const middleware = createKismetMiddleware({
  collectionSlug: process.env.KISMET_COLLECTION_SLUG!,
  trackingKey: process.env.KISMET_TRACKING_KEY!,
  // Your consent manager's cookie. Consent gates the session, not the recording.
  consent: consentFromCookie('CookieConsent', /statistics:true/),
  // Which URLs are what, once. Your own listing id is fine: send it as
  // externalListingId; ingest resolution requires Contract 1.1.
  profile: {
    property: { pattern: /^\/stays\/[^/]+\/([^/]+)\/?$/, as: 'externalListingId' },
    searchPaths: ['/stays', /^\/stays\/in\//],
    intent: { path: '/stays/checkout', checkinParam: 'checkin', checkoutParam: 'checkout', guestsParam: 'guests' },
  },
});

export const config = { matcher: KISMET_MATCHER };
```

`KISMET_MATCHER` covers pages and the two agent index files (`/llms.txt`, `/.well-known/llm-index.json`) and never assets or `/api/*`.

Already have a middleware? Pass it as `next`. Telemetry resolves the visitor first, calls yours with the seed headers on the request, and adds its cookies and headers to whatever you return:

```ts
export const middleware = createKismetMiddleware({
  ...config,
  next: async (request, event, visitor) => {
    // visitor.kidSid, visitor.tier and visitor.classification are available here
    return NextResponse.next({ request: { headers: request.headers } });
  },
});
```

## 2. The seed

In the root layout, first thing in `<head>`:

```tsx
// app/layout.tsx
import { KismetSeed } from '@kismet-tech/telemetry-next/seed';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <head>
        <KismetSeed collectionSlug={process.env.KISMET_COLLECTION_SLUG!} />
      </head>
      <body>{children}</body>
    </html>
  );
}
```

`KismetSeed` reads the headers the middleware attached, which makes the layout dynamic. That is what you want: a seeded page must never be statically cached. It renders one inline script that sets the seed and then appends the browser tracker tag itself, because React 19 hoists `<script async src>` elements ahead of other head content and a separate tag would load before the seed.

## Returning-visitor recognition

After Kismet enables your collection, add `visitorRecognition: true` to the middleware configuration above and retain the explicit consent hook. `KismetSeed` automatically makes the same-origin follow-up that receives `_kid_vid`. No additional application API route is required.

The default matcher includes `/__kismet/visitor`; include it in custom matchers and exclude it from shared caches. See the [returning-visitor guide](https://developers.kismet.travel/telemetry/visitor-recognition.md) for base paths, Pages Router and cookie recovery checks.

## 3. The checkout call

From the server-side handler that knows the reservation succeeded, once:

```ts
import { bookingBridge } from '@kismet-tech/telemetry-next';

await bookingBridge(config, { kidSid, confirmationCode, bookingEngine: 'custom', domain: 'example.co.uk' });
```

`kidSid` is the `_kid_sid` cookie on the checkout request; carry it through your booking pipeline to wherever the confirmation code is known. The call is bounded and never throws; it must not affect the booking.

## Slug-only property URLs

The `after()` example below requires Next.js 15.1 or later. On earlier supported versions, use the runtime's supported background scheduler or the existing middleware path.

When the listing id is only known at render (the URL has no id the middleware can read), record the property view from the page's server component:

```ts
import { headers } from 'next/headers';
import { after } from 'next/server';
import { trackServerPropertyView } from '@kismet-tech/telemetry-next';

after(trackServerPropertyView(config, {
  headers: await headers(), url: currentUrl,
  externalListingId: listing.id, checkIn, checkOut, guests,
}));
```

## Browser signals the server cannot see

Saves, dates picked in a picker, the book button. The client helpers dispatch the events the browser tracker bridges; never call `window.Kismet.track()` directly.

```ts
import { createTracker } from '@kismet-tech/telemetry-next/client';
const track = createTracker({ collectionSlug: 'your-collection' });

track.propertyView({ externalListingId: listing.id, checkIn, checkOut, guests, stayTotalCents });
track.save({ externalListingId: listing.id });
track.bookIntent({ externalListingId: listing.id, checkIn, checkOut, guests, stayTotalCents });
```

## What the middleware does on every page request

Resolves the visitor (threaded id, cookie, suppressed for bots and visitors without consent, else a locally minted id reconciled with Kismet after the response), emits one server-plane event (agent surfaces as `fetch` with no identity), sets `_kid_sid` on the dotted serving domain; optional visitor recognition persists `_kid_vid` through a bounded follow-up request, marks the response `private, no-store`, and passes the seed to the layout through request headers. The page never waits on Kismet, and a middleware error serves the page without telemetry rather than failing it. The [overview](https://developers.kismet.travel/telemetry.md#how-it-works) walks through the six steps; the [contract](https://developers.kismet.travel/telemetry/contract.md) specifies them.

## Verify

1. Load a page in a fresh browser. Expect a `_kid_sid` cookie on the dotted domain and `window.Kismet._kidSid` equal to it, with the response header `x-kismet-anchor-tier: minted`.
2. Reload with consent retained. The same session is adopted; an enabled visitor follow-up may set or refresh `_kid_vid`.
3. `curl -A GPTBot https://your-host/some-page`: no `Set-Cookie`, and the page seeds `_sidSuppressed`.
4. `curl https://your-host/llms.txt`: recorded as a fetch with no session.
5. From a consent jurisdiction without consent: no cookie, page still recorded.
6. If using the booking bridge, complete a staging test booking and verify its successful response and session join.
7. With visitor recognition enabled, restart the browser, remove only `_kid_sid`, and visit again. Expect the same `_kid_vid` with a new session; Kismet can verify the stored visitor link. Withdraw consent and confirm both cookies are removed on the next adapter request.

The package's own tests run it through the contract's conformance suite with a real `NextRequest`. To run the suite against your own middleware in CI:

```js
import { runConformance, formatReport } from '@kismet-tech/telemetry/conformance';
const report = await runConformance((env) => makeYourDriver(env));
if (!report.ok) throw new Error(formatReport(report));
```

## Without the package

The adapter is about two hundred lines over the core. If you would rather own them, `src/conformance/reference-adapter.js` inside `@kismet-tech/telemetry` is the same logic as a plain `Request` to `Response` handler, and the [contract](https://developers.kismet.travel/telemetry/contract.md) is the specification it implements.

## Pin the browser tracker

To review browser code before it changes on your site, configure the [pinned browser tracker](https://developers.kismet.travel/telemetry/pinned-browser-tracker.md). Pinning the npm package or WordPress plugin alone does not pin the default `k.js` URL. The pinned release works with version 1.1.0 and preserves your existing consent and returning-visitor configuration.

## Connect an existing consent banner

Follow the [consent integration guide](https://developers.kismet.travel/telemetry/consent.md) to map your saved banner choice to both server adapters and browser tracker 1.2.0. Kismet can prepare the configuration if you share your CMP, consent-cookie format and saved-choice callback.
