Migrating to Next.js App Router

This guide will help you migrate your Plasmic Next.js-based codebases from the pages router to the app router.

For the full reference, see the example codebase in the Plasmic/Prisma integration repository.

Minimum Requirements

Make sure that you are using the latest version of @plasmicapp/ packages, as app router support was added in the newest major version update.

Also, this guide assumes that you already updated the non-Plasmic core of your codebase to be compatible with app router, e.g. by following the official Next.js migration guide.

New plasmic-init-client.tsx initialization file

App Router requires splitting your initialization into two files to separate server and client concerns.

Keep plasmic-init.ts as the server-facing loader file, and put registerFunction() calls there so they stay available from server-rendered routes.

Copy
import { initPlasmicLoader } from '@plasmicapp/loader-nextjs/react-server-conditional';
export const PLASMIC = initPlasmicLoader({
projects: [{ id: 'YOUR_PROJECT_ID', token: 'YOUR_API_TOKEN' }],
preview: false
});
// Register server-side functions here, e.g.:
// PLASMIC.registerFunction(myFetchFn, { name: 'myFetchFn', ... });

Create plasmic-init-client.tsx as the client-side wrapper for registrations that belong in Client Components, such as code components, contexts, and tokens.

Copy
'use client';
import { PlasmicRootProvider } from '@plasmicapp/loader-nextjs';
import { PLASMIC } from './plasmic-init';
// Register client-side code components here, e.g.:
// PLASMIC.registerComponent(MyComponent, { name: 'MyComponent', ... });
export function PlasmicClientRootProvider(props: Omit<React.ComponentProps<typeof PlasmicRootProvider>, 'loader'>) {
return <PlasmicRootProvider loader={PLASMIC} {...props} />;
}

Data fetching

usePlasmicQueryData still works in App Router, but data fetched with it will not be rendered on the server side (SSR) due to React Server Components constraints.

Instead, register your data-fetching functions using registerFunction in plasmic-init.ts. They become available as data queries in Plasmic Studio and run in the current render context. When a data query runs during a server render, its result is hydrated to the client, so it does not immediately run again on the client unless its inputs change.

Before:

Copy
import { usePlasmicQueryData } from '@plasmicapp/loader-nextjs';
function MyComponent() {
const { data } = usePlasmicQueryData('/my-query', () => fetchMyData());
// ...
}

After (in plasmic-init.ts):

Copy
PLASMIC.registerFunction(fetchMyData, {
name: 'fetchMyData',
// This makes the function available as a data query in Plasmic Studio
isQuery: true
// ...
});

Then use $q.fetchMyData in Plasmic Studio to wire up the data.

Page metadata

Replace <Head> with Next.js’s generateMetadata export.

To output Plasmic metadata, including dynamic values, define generateMetadata() in the same route file and reuse the same page-data helper that your page uses.

Before:

Copy
import Head from 'next/head';
export default function Page({ plasmicData }) {
const pageMeta = plasmicData.entryCompMetas[0];
return (
<>
<Head>
<title>{pageMeta.pageMetadata?.title}</title>
</Head>
{/* ... */}
</>
);
}

After:

Copy
import { Metadata, ResolvingMetadata } from 'next';
import { PLASMIC } from '../../plasmic-init';
type LoaderPageProps = {
params?: { catchall?: string[] };
};
async function getPageData(params?: LoaderPageProps['params']) {
const plasmicPath = '/' + (params?.catchall ? params.catchall.join('/') : '');
const componentData = await PLASMIC.maybeFetchComponentData(plasmicPath);
return { componentData };
}
export async function generateMetadata({ params }: LoaderPageProps, parent: ResolvingMetadata): Promise<Metadata> {
const { componentData } = await getPageData(params);
if (!componentData || componentData.entryCompMetas.length === 0) {
return parent as Promise<Metadata>;
}
const pageMeta = componentData.entryCompMetas[0];
const metadata = await PLASMIC.unstable__generateMetadata(componentData, {
params: pageMeta.params ?? {},
query: {}
});
return { ...(await parent), ...metadata };
}

SSG for dynamic routes

Replace getStaticPaths with generateStaticParams.

Before:

Copy
import { GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async () => {
const pages = await PLASMIC.fetchPages();
return {
paths: pages.map((page) => ({
params: { catchall: page.path.substring(1).split('/') }
})),
fallback: 'blocking'
};
};

After:

Copy
export async function generateStaticParams() {
const pageModules = await PLASMIC.fetchPages();
return pageModules.map((mod) => {
const catchall = mod.path === '/' ? undefined : mod.path.substring(1).split('/');
return {
catchall
};
});
}

Is this page helpful?

NoYes