Next.js

The Quick Start works in the App Router. Two things are specific to Next.js: where the provider lives, and how commands come from your routes.

1. A client file for the palette

The provider, the palette and the shortcut use state and effects, so they run in a Client Component. Put them in one file that starts with 'use client', together with the provider config:

app/command-menu.tsx
'use client'

import { useMemo, type ReactNode } from 'react'
import { useRouter } from 'next/navigation'
import { CommandEngineProvider, useCommandRegister } from 'cmdk-engine/react'
import { CommandPalette, useCommandPaletteShortcut } from 'cmdk-engine/adapters/cmdk'
import { sitemapToCommands } from 'cmdk-engine/adapters/sitemap'
import sitemap from '@/generated/command-routes.json'

const routeCommands = sitemapToCommands(sitemap)

function Palette() {
  useCommandRegister(routeCommands)
  useCommandPaletteShortcut() // Cmd+K / Ctrl+K
  return <CommandPalette dialog placeholder="Search..." />
}

export function CommandMenu({ children }: { children: ReactNode }) {
  const router = useRouter()
  const config = useMemo(() => ({ onNavigate: (href: string) => router.push(href) }), [router])

  return (
    <CommandEngineProvider config={config}>
      {children}
      <Palette />
    </CommandEngineProvider>
  )
}

Render it from the root layout, which stays a Server Component:

app/layout.tsx
import type { ReactNode } from 'react'
import { CommandMenu } from './command-menu'

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <CommandMenu>{children}</CommandMenu>
      </body>
    </html>
  )
}

Why the config stays in the client file

A Server Component can pass only serializable props to a Client Component. onNavigate, onSelect, t, asyncSources and access-control providers are functions, so a config written in a server layout fails the build ("Functions cannot be passed directly to Client Components"). Define the config in the 'use client' file: at module scope, or with useMemo when it needs a hook such as useRouter.

2. Commands from your routes

The CLI scans your app/ directory. Run it before each build:

package.json
{
  "scripts": {
    "prebuild": "cmdk-engine scan --framework nextjs-app"
  }
}

It writes src/generated/command-routes.json, which the @/ import above reads in a src/ layout; set output in cmdk-engine.config.ts to write it elsewhere. Every page file becomes a route:

  • route groups and @slot folders add no segment;
  • private _folders, intercepting (.) routes and api/ are skipped;
  • [[...slug]] gives its parent's URL, and other dynamic routes are skipped unless you name them (below).

3. Apps under [locale]

When every page sits under a dynamic segment such as app/[locale], name it so the scan keeps those routes:

cmdk-engine.config.ts
import { defineConfig } from 'cmdk-engine'

export default defineConfig({
  framework: 'nextjs-app',
  includeDynamic: ['locale'],
})

app/[locale]/billing/page.tsx then becomes /:locale/billing. Fill the segment with the current locale when you register the commands:

app/[locale]/command-menu.tsx
function RouteCommands() {
  const { locale } = useParams<{ locale: string }>()
  const commands = useMemo(() => sitemapToCommands(sitemap, { params: { locale } }), [locale])
  useCommandRegister(commands)
  return null
}

params values are inserted as given. Pass '' to drop the segment, for a default locale served without a prefix or a router that adds the prefix itself. Ids keep the placeholder (locale--billing), so frecency and Recent are shared across locales.

Pages Router

Scan with --framework nextjs-pages, render the provider in pages/_app.tsx, and navigate with useRouter from next/router. The Pages Router needs no 'use client'.

A runnable example

The Next.js App Router example has these files, with [locale] pages. Copy the folder, or open it in StackBlitz. More patterns are on the Examples page.