Next.js
Mount Docent in a Next.js App Router app, with the client boundary, useNextAdapter, the environment variables, and where the component belongs.
Docent runs in the browser, so the mount is a client component. Put it in its own file, and render that file from the root layout so it wraps everything.
The client component
'use client'
import type { ReactNode } from 'react'
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
import { Docent } from '@usedocent/react'
import { useNextAdapter } from '@usedocent/react/next'
import { PRESENCE_LOADERS } from '@usedocent/mascot/library/loaders'
export const Guide = ({ userId, children }: { userId: string; children: ReactNode }) => {
const router = useNextAdapter(useRouter(), usePathname(), useSearchParams())
return (
<Docent
appId={process.env.NEXT_PUBLIC_DOCENT_APP_ID!}
publishableKey={process.env.NEXT_PUBLIC_DOCENT_PUBLISHABLE_KEY!}
apiUrl="https://api.usedocent.com"
user={{ id: userId }}
router={router}
loadPresence={(slug) => PRESENCE_LOADERS[slug]?.() ?? Promise.reject(new Error(slug))}
loadAudio={(vendor) =>
vendor === 'openai' ? import('@usedocent/sdk-audio-live') : import('@usedocent/sdk-audio')
}
voice="tap"
>
{children}
</Docent>
)
}The adapter
useNextAdapter takes the results of the three hooks rather than importing next/navigation
itself, so installing Docent does not put Next in the dependency graph of an app that is not
using it. Its signature is:
useNextAdapter(
router: { push: (href: string) => void },
pathname: string,
searchParams?: { toString: () => string } | null,
): RouterAdapterIt navigates with push, not replace, so a page the guide took the user to leaves a back
button that returns them.
searchParams is optional and worth passing: Docent's route matching includes the query string,
and usePathname alone reports /settings for /settings?tab=keys. Reading useSearchParams
opts the component into client-side rendering under static export, which is why the adapter does
not require it. If that is a problem, pass an object that reads the live location instead:
const search = useRef({ toString: () => globalThis.location?.search.replace(/^\?/, '') ?? '' })
const router = useNextAdapter(useRouter(), usePathname(), search.current)The adapter calls toString() when it needs the path, not during render, so this stays fresher
than a hook result captured a render ago.
The root layout
import { Guide } from './guide'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Guide userId={userId}>{children}</Guide>
</body>
</html>
)
}Wrapping matters. A <Guide /> rendered beside the page mounts a working guide and a
useDocentTrack() that silently does nothing, because the context only reaches children, and a
goal whose event never fires looks exactly like a goal nobody completed.
Environment variables
NEXT_PUBLIC_DOCENT_APP_ID=your-app-id
NEXT_PUBLIC_DOCENT_PUBLISHABLE_KEY=dk_pub_your_key
DOCENT_SECRET_KEY=dk_sec_your_keyOnly the first two reach the browser, and both are meant to. The secret key uploads routes and
knowledge, so it has no NEXT_PUBLIC_ prefix and stays on the server.
Pages the guide should skip
The mount re-keys its session on appId, publishableKey, apiUrl, user.id and userToken,
so returning the children bare on the routes you want it off is the way to skip them. Hiding a
mounted guide with CSS is not: it has already minted a session, and it will still load its audio
chunk on a tap.
const NO_GUIDE = ['/sign-in', '/sign-up']
const allowed = (p: string) => !NO_GUIDE.some((x) => p === x || p.startsWith(`${x}/`))What to do when the variables are missing
A fork or a fresh checkout without the environment file should get an app with no guide, not a crash:
if (!APP_ID || !KEY || !API) return <>{children}</>Quickstart
Ten steps from an empty Console to a working guide in your app, covering keys, the mount, markers, origins, knowledge, goals, presence and voice.
React Router and Remix
Mount Docent in a React Router or Remix app with useReactRouterAdapter, built from the useNavigate and useLocation hooks you already have.
