Upgrade starters to App Router
Package
basic-starter
Describe the feature request
The current starters both use the Pages directory for routing.
Describe the solution you'd like
We should clone the basic-starter to pages-starter. And clone graphql-starter to pages-graphql-starter.
And then we should upgrade the old starters to use the App Router.
Thank you for providing this project, it is greatly appreciated.
Currently, I am exploring the possibility of converting the starter to the app router using the latest library, specifically the one found at https://www.npmjs.com/package/next-drupal/v/0.0.0-pr.600.128d03bc.
My objective at the moment is to gain a better understanding of how to migrate an existing project.
So far everything is working fine, however, I believe I may be missing something important.
Several functions utilize the context object, such as getResourceFromContext, which originates from the pages router, as seen here: https://github.com/chapter-three/next-drupal/blob/main/packages/next-drupal/src/get-resource.ts#L17.
How should I proceed with the conversion process? Would it be necessary to create a custom context object?
Another example of the context object is this one translatePathFromContext() which basically expects an object with the slug attribute: https://github.com/chapter-three/next-drupal/blob/main/packages/next-drupal/src/client.ts#L962
Trying to convert to App Router, where we don't have anymore the context object, the only way I found to convert this one https://github.com/chapter-three/next-drupal-basic-starter/blob/main/pages/%5B...slug%5D.tsx, is:
export default async function Page({ params }: any) {
const resource = await getPage(params);
return (
<Layout>
<Head>
<title>{resource.title}</title>
<meta name="description" content="A Next.js site powered by Drupal." />
</Head>
{resource.type === "node--page" && <NodeBasicPage node={resource} />}
{resource.type === "node--article" && <NodeArticle node={resource} />}
</Layout>
);
}
async function getPage(args: any) {
const path = await drupal.translatePathFromContext({ params: args });
if (!path) {
return {
notFound: true,
};
}
const type = path.jsonapi?.resourceName;
let params = {};
if (type === "node--article") {
params = {
include: "field_image,uid",
};
}
const resource = await drupal.getResourceFromContext<DrupalNode>(path, args, {
params,
});
// At this point, we know the path exists and it points to a resource.
// If we receive an error, it means something went wrong on Drupal.
// We throw an error to tell revalidation to skip this for now.
// Revalidation can try again on next request.
if (!resource) {
throw new Error(`Failed to fetch resource: ${path.jsonapi?.individual}`);
}
return resource;
}
It mostly works, but yet, not sure if I'm (ab)using the API by manually crafting the context object.
@JohnAlbin sorry for coming back on this. Can you help me to understand how the context object should be treated in the app router context ?
The context object is what I'm struggling with at the moment as well.
Let's walk through the old pages/[...slug].tsx flow so we all (including me) understand it.
How the old pages/[...slug].tsx works
Next.js runs getStaticPaths
getStaticPathsreceives a context object:type GetStaticPathsContext = { locales?: string[] defaultLocale?: string }getStaticPathscallsdrupal.getStaticPathsFromContext(RESOURCE_TYPES, context)getStaticPathsreturnspathswhich is an array of strings representing aslugfor each page.
Next.js runs getStaticProps per entry in the paths array.
getStaticPropsreceives a slightly different context object:
Thetype GetStaticPropsContext = { params?: Params preview?: boolean previewData?: Preview draftMode?: boolean locale?: string locales?: string[] defaultLocale?: string }context.paramsis one of the items in thepathsarray returned bygetStaticPathsgetStaticPropsthen callsdrupal.translatePathFromContext(context)translatePathFromContextusescontext.params?.slugRED FLAGslugis a convention, not a specific property name. If the name of this file was[...path], the param would bepathnotslug. Also, looking at Next.js docs, this param is supposed to be an array of strings (each part in a path likesome/slug1/slug2), not a single string; but ourdrupal.getStaticPathsFromContextreturns a singlestringand not astring[].translatePathFromContextreturnspath: DrupalTranslatedPath
getStaticPropsgets the entity type frompath?.jsonapi?.resourceNameso that it knows which fields it should be asking for when it requests the entity and which React component to use to render the entity.getStaticPropscallsdrupal.getResourceFromContext( path, context, { params })wherepathis aDrupalTranslatedPath,contextis aGetStaticPropsContext, andparamsis the list of fields, sorting, etc.getStaticPropsreturns theresourcethat is returned from the previous step.
For each call to getStaticProps, Next.js renders the NodePage component with the resource prop returned from that getStaticProps call.
How a new app/[...slug]/page.tsx should work
Next.js runs generateStaticParams
-
generateStaticParamsis given anoptionsobject that contains theparamsfrom any parent directory dynamic routes. See https://nextjs.org/docs/app/api-reference/functions/generate-static-params#parameters In thenext-drupalstarters, there's no parent dynamic route, soparamsis empty. -
generateStaticParamsshouldn't calldrupal.getStaticPathsFromContext(RESOURCE_TYPES, context)because there is nocontextobject. We'll need a new method to use and one doesn't exist right now. But in the interim, we can calldrupal.getStaticPathsFromContext(RESOURCE_TYPES,{}) -
generateStaticParamsreturns an array ofparamsobjects where one of the param property names MUST match the filename if it's a dynamic route filename. examples:- If used is in
[category].tsx, the returned data should match{ category: string }[] - If used is in
[...slug].tsx, the returned data should match{ slug: string[] }[]
That means we'll need to let the developer massage the data returned by
drupal.getStaticPathsFromContext()(or what we replace it with). - If used is in
Next.js has removed the old getStaticProps step. So all of the old sub-steps need to be moved to the next step.
Lastly, Next.js renders a page using the NodePage component for each entry in the params array returned by generateStaticParams
NodePagewill need to check if draft mode is on withdraftMode().isEnabledand, if it is, then figure out how to determine which revision Drupal wants us to show (TBD: Cookies?).NodePageshould calldrupal.translatePath(params.slug[0])to get thepath: DrupalTranslatedPathNodePagegets the entity uuid frompath.entity.uuidand the entity type frompath?.jsonapi?.resourceNameso that it knows which fields it should be asking for when it requests the entity and which React component to use to render the entity.NodePagecallsdrupal.getResource(type, uuid, { params })with the path or uuid and the revision ID of the desired entity andparams, which is the list of fields, sorting, etc.NodePagethen finishes rendering the returned entity.
@JohnAlbin, first of all, let me say thank you so much for your super detailed answers. Give me some time to connect all the dots, and hopefully, I can contribute back with some code too.
@JohnAlbin I've created a diagram of the Pages Router flow. I would appreciate your feedback before I continue with the other flow.
You can find it here: https://excalidraw.com/#room=f28794c0ac45452180bf,1OOdKSnoZByreHn6SkkZhA
And here as an attachment, just remove .txt part of the extension. next-drupal-diagram-1.excalidraw.txt
I've just added to the diagram even the new app router workflow, as you've described, it is the same link and attaached.
I've updated my comment above showing “NodePage calls drupal.getResource(type, uuid, { params })”. I figured that out when adding the examples/example-router-migration to the next-drupal repo. https://github.com/chapter-three/next-drupal/tree/main/examples/example-router-migration