Next.js Pages → App Router
Days Med riskMove a Next.js Pages-Router app to the App Router incrementally. The two routers can coexist (the App Router takes precedence for matching routes), so you don’t need a flag day.
This guide focuses on the Mushi-specific bits — provider placement, CSP for static export, server-component vs client-component boundaries. For the framework-level migration mechanics see the official Next.js migration guide .
Same Mushi project, same API key. The provider import changes from
pages/_app.tsx to app/providers.tsx, but everything downstream of it
works the same.
API mapping (Pages → App)
| Pages Router | App Router |
|---|---|
pages/_app.tsx (<MushiProvider> mounted here) | app/providers.tsx (client component) imported in app/layout.tsx |
pages/_document.tsx | app/layout.tsx |
getServerSideProps | Server Component fetch |
getStaticProps / getStaticPaths | generateStaticParams + Server Component fetch |
useRouter().query | useSearchParams() + useParams() |
next/head | metadata export OR <head> in layout |
Migration checklist
- Step 01Audit dependencies for App Router compatibility
- Step 02Create the app/ directory alongside pages/
- Step 03Create app/layout.tsx (the new root)
- Step 04Create app/providers.tsx with the Mushi provider
- Step 05Delete pages/_app.tsx (only AFTER you have an app/layout.tsx)
- Step 06Port routes one at a time
- Step 07Convert data fetching to Server Components
- Step 08Update CSP for App Router
- Step 09Re-test static export (if applicable)
- Step 10Smoke-test on every primary route
Where to put <MushiProvider> (the most asked question)
At the root of your tree, in a 'use client' component, mounted by the
root layout. This means:
- ✅
app/providers.tsx(client component) imported intoapp/layout.tsx - ❌ Inside an individual page (loses provider context across navigations)
- ❌ As a Server Component (the SDK uses React state — must be client)
The cost of one client boundary at the root is minimal (the rest of your
tree can still be server-rendered) and it matches how Next-Auth’s
SessionProvider, react-query’s QueryClientProvider, and similar libs
are placed.
Server Components and useMushi()
useMushi(), useMushiReport(), and the visual widget all require a
client component. That’s expected — Mushi captures user-side context
(console, network, screenshot) which only exists in the browser.
If you need user-facing context inside a Server Component (e.g. to pre-render a “Report a problem with this article” button with a contextual route), pass the data down as props and let the client child call Mushi:
// app/articles/[slug]/page.tsx (Server Component)
export default async function Page({ params }: { params: { slug: string } }) {
const article = await fetchArticle(params.slug)
return <ArticleReportButton articleId={article.id} />
}
// app/articles/[slug]/ArticleReportButton.tsx (Client Component)
'use client'
import { useMushiReport } from '@mushi-mushi/react'
export function ArticleReportButton({ articleId }: { articleId: string }) {
const { submit } = useMushiReport()
return <button onClick={() => submit({ description: 'Article issue', metadata: { articleId } })}>Report</button>
}Common gotchas
- Two providers mounted. If you forget to delete
pages/_app.tsxafter creatingapp/layout.tsx, both run, which double-fires events. Always remove the old one in the same PR. useMushi()in a Server Component. Build error. Add'use client'to the file or move the hook into a child client component.- Stale env vars after rename. App Router enforces the
NEXT_PUBLIC_*prefix more strictly than Pages did; non-prefixed vars are now strictly server-only. Rename if migrating from a Pages app that relied onprocess.env.MUSHI_*.