# Kibble installation contract: section 17 of 29 (guide version 3)

> This is **one section only**, not the whole contract. Complete document: https://www.usekibble.dev/AI/guide.md

## 17. Next.js App Router {#nextjs}

> **Status: direct integration.** The real SDK is used as written; exercised in the repository demos and browser tests.

- Install once in the root `app/layout.tsx`. Route changes are observed automatically: the initial load is one `page_view` (`nav: load`), each client-side navigation one more (`nav: spa`). **Do not** add your own page-view calls.
- Do not install a second time inside a nested layout or a client component that remounts.
- The inline queue also re-applies the stored consent choice before the SDK loads, so the first page view is not lost. The banner (a client component) calls `setKibbleConsent`.
- Register the dev origin including its port (for example `localhost:3000`) as a host of the property.

app/layout.tsx:

```tsx
import Script from 'next/script';
import { KibbleConsent } from './KibbleConsent';
import { KIBBLE_PRELOAD } from './kibble-consent';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Script id="kibble-queue" strategy="beforeInteractive">{KIBBLE_PRELOAD}</Script>
        {children}
        <KibbleConsent />
        <Script
          src="https://use.example-kibble-host/sdk/v1/kibble.js"
          strategy="afterInteractive"
          data-key="pk_YOUR_PROPERTY_KEY"
          data-endpoint="https://use.example-kibble-host"
          data-consent="manual"
        />
      </body>
    </html>
  );
}
```

kibble-consent.ts:

```ts
export const CONSENT_KEY = 'YOUR_CONSENT_KEY';
export const KIBBLE_PRELOAD = `window.kibble=window.kibble||function(){(window.kibble.q=window.kibble.q||[]).push(arguments)};
try{var c=localStorage.getItem('${CONSENT_KEY}');if(c==='granted'||c==='denied')kibble('consent',c)}catch(e){}`;

export function setKibbleConsent(choice: 'granted' | 'denied') {
  try { localStorage.setItem(CONSENT_KEY, choice); } catch {}
  (window as any).kibble?.('consent', choice);   // 'denied' stops, purges the queue and deletes the browser id
}
export async function forgetMe() {
  try { localStorage.setItem(CONSENT_KEY, 'denied'); } catch {}
  await (window as any).kibble?.('forget');
}
```

Per-page content ids and types come from `<meta name="kibble:content">` tags ([Semantic markup](https://www.usekibble.dev/AI#markup)). Render them in the server-side head (in the App Router, the `metadata` export's `other` field is one way). The SDK re-reads the title and meta about 50 ms after a client-side navigation, so metadata that a framework applies later is missed and the page falls back to `page:{path}`; confirm in Validation that the content id appears. Add `data-connected-hosts` and `data-datalayer="on"` to the Script only when you need them.

END OF SECTION nextjs
