Next.js Pages → App Router
Days Med riskMove a Next.js Pages-Router app to the App Router incrementally. The two routers can run in the same app, one route at a time, so you don’t need a flag day. A given URL can live in only one of them.
This guide focuses on the Mushi Mushi bits: provider placement, CSP for static export, and server-component vs client-component boundaries. For the framework-level migration mechanics see the official Next.js migration guide .
Same Mushi Mushi project, same API key. App routes get the provider from
app/providers.tsx; routes still in pages/ keep getting it from
pages/_app.tsx until you move them. Everything downstream 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 Mushi provider
- Step 05Keep pages/_app.tsx until the last pages/ route is moved
- 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 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 Mushi:
// app/articles/[slug]/page.tsx (Server Component)
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params // params is a Promise in Next.js 15+
const article = await fetchArticle(slug)
return <ArticleReportButton articleId={article.id} />
}
// app/articles/[slug]/ArticleReportButton.tsx (Client Component)
'use client'
import { useMushiSdk } from 'mushi-mushi/react'
export function ArticleReportButton({ articleId }: { articleId: string }) {
const mushi = useMushiSdk()
return <button onClick={() => void mushi?.captureEvent({ description: 'Article issue', metadata: { articleId } })}>Report</button>
}Common gotchas
- Deleting
pages/_app.tsxtoo early. Routes still inpages/lose their providers, including Mushi Mushi’s. Delete_apponly after the lastpages/route has moved. - Navigating between the two routers is a full page load. The Mushi Mushi SDK starts again on the other side; nothing else to do.
useMushi()in a Server Component. Build error. Add'use client'to the file or move the hook into a child client component.- Env vars. Only
NEXT_PUBLIC_*variables reach the browser, in both routers. If a Pages app passed a server-onlyMUSHI_*value down throughgetServerSideProps, rename it toNEXT_PUBLIC_MUSHI_*and read it in the client provider.