next.js icon indicating copy to clipboard operation
next.js copied to clipboard

Next 13 - generateStaticParams not working with Multiple Dynamic Segments in a Route

Open ClemannD opened this issue 1 year ago • 4 comments

Verify canary release

  • [X] I verified that the issue exists in the latest Next.js canary release

Provide environment information

Reproduced in multiple envs, but pasting the GitHub Codespace

Operating System:
  Platform: linux
  Arch: x64
  Version: #100~18.04.1-Ubuntu SMP Mon Oct 17 11:44:30 UTC 2022
Binaries:
  Node: 16.18.0
  npm: 8.19.2
  Yarn: 1.22.19
  pnpm: N/A
Relevant packages:
  next: 13.0.3
  eslint-config-next: 13.0.3
  react: 18.2.0
  react-dom: 18.2.0

What browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

No response

Describe the Bug

I am following the new docs for generating pages with Multiple Dynamic Segments in a Route (https://beta.nextjs.org/docs/api-reference/generate-static-params#generate-segments-from-the-top-down).

There is an issue when generating the child routes of the second dynamic route.

I am providing a minimum reproduction in a clean repo which also experiences the issue: https://github.com/ClemannD/next-13-test-repo. (this was created using npx create-next-app@latest --experimental-app)

here is the output from npm run build:

@ClemannD ➜ /workspaces/next-13-test-repo (main ✗) $ npm run build

> [email protected] build
> next build

warn  - You have enabled experimental feature (appDir) in next.config.js.
warn  - Experimental features are not covered by semver, and may cause unexpected or broken application behavior. Use at your own risk.
info  - Thank you for testing `appDir` please leave your feedback at https://nextjs.link/app-feedback

info  - Creating an optimized production build  
info  - Compiled successfully
info  - Linting and checking validity of types  
info  - Collecting page data ..categorySlug undefined
Error: A required parameter (categorySlug) was not provided as a string in generateStaticParams for /[categorySlug]/[productId]
    at /workspaces/next-13-test-repo/node_modules/next/dist/build/utils.js:656:27
    at Array.forEach (<anonymous>)
    at /workspaces/next-13-test-repo/node_modules/next/dist/build/utils.js:649:29
    at Array.forEach (<anonymous>)
    at buildStaticPaths (/workspaces/next-13-test-repo/node_modules/next/dist/build/utils.js:620:17)
    at buildAppStaticPaths (/workspaces/next-13-test-repo/node_modules/next/dist/build/utils.js:786:16)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async /workspaces/next-13-test-repo/node_modules/next/dist/build/utils.js:878:119
    at async Span.traceAsyncFn (/workspaces/next-13-test-repo/node_modules/next/dist/trace/trace.js:79:20)

> Build error occurred
Error: Failed to collect page data for /[categorySlug]/[productId]
    at /workspaces/next-13-test-repo/node_modules/next/dist/build/utils.js:959:15
    at processTicksAndRejections (node:internal/process/task_queues:96:5) {
  type: 'Error'
}
info  - Collecting page data

These are the files i have

// app/[categorySlug]/page.tsx

export default async function CategoryPage({
  params,
}: {
  params: { categorySlug: string };
}) {
  return <div>{params.categorySlug}</div>;
}

export async function generateStaticParams() {
  return [
    {
      categorySlug: "food",
    },
    {
      categorySlug: "materials",
    },
  ];

}
// app/[categorySlug]/[productId]/page.tsx

export default async function ProductPage({
  params,
}: {
  params: { productId: string };
}) {
  console.log("params", params);
  return <div></div>;
}

export async function generateStaticParams({ categorySlug }: any) {
  console.log("categorySlug", categorySlug);

  return [
    {
      productId: "1",
    },
    {
      productId: "2",
    },
  ];
}

Expected Behavior

According to the docs, this is the way to generate the following routes:

  • /food/1
  • /food/2
  • /materials/1
  • /materials/2

Link to reproduction - Issues with a link to complete (but minimal) reproduction code will be addressed faster

https://github.com/ClemannD/next-13-test-repo

To Reproduce

Open a codespace, npm install && npm run build

ClemannD avatar Nov 12 '22 23:11 ClemannD

I am running into problems as well. I am trying the top-down approach. Generate segments from the top down

categorySlug prop passed by parent segment's 'generateStaticParams' function - the slug is coming back as undefined.

Looks like the same problem you are running into. The categorySlug is not getting passed down to the child's generateStaticParams.

trevorpfiz avatar Nov 13 '22 15:11 trevorpfiz

For what it's worth, the Bottom Up approach does work.

If I change the ProductPage to have

export async function generateStaticParams(categorySlug: any) {
  console.log("categorySlug", categorySlug);
  return [
    {
      categorySlug: "food",
      productId: "1",
    },
    {
      categorySlug: "food",
      productId: "2",
    },
    {
      categorySlug: "materials",
      productId: "1",
    },
    {
      categorySlug: "materials",
      productId: "2",
    },
  ];
}

it generates as expected:

Route (app)                                Size     First Load JS
┌ ○ /                                      0 B                0 B
├ λ /[categorySlug]                        137 B          66.2 kB
└ ● /[categorySlug]/[productId]            139 B          66.2 kB
    ├ /food/1
    ├ /food/2
    ├ /materials/1
    └ /materials/2
+ First Load JS shared by all              66.1 kB
  ├ chunks/17-adeb71b82e6fb442.js          63.8 kB
  ├ chunks/main-app-f7991bdf5a7d528d.js    200 B
  └ chunks/webpack-d2d5d8dda7543a35.js     2.06 kB

ClemannD avatar Nov 16 '22 00:11 ClemannD

Good to know! Will have to implement this. Thanks!

trevorpfiz avatar Nov 16 '22 14:11 trevorpfiz

It also not work for catch all segments: A required parameter (slug) was not provided as an array in generateStaticParams for /[...slug]

Return of generateStaticParams is: [ { slug: '/f' }, { slug: '/e' }, { slug: '/d' }, { slug: '/c' }, { slug: '/b' }, { slug: '/a' } ]

semy avatar Nov 18 '22 02:11 semy

I am not sure if this is a bug or a feature.

It seems that only the output of the generateStaticParams in [categorySlug]/layout.tsx is forwarded as an argument to [categorySlug]/[productId]/page.tsx.

The generateStaticParams in [categorySlug]/page.tsx will generate paths and override [categorySlug]/layout.tsx only for that parent segment.

In case of your test repo, moving the generateStaticParams from [categorySlug]/page.tsx to [categorySlug]/layout.tsx will generate this:

┌ ○ /                                      0 B                0 B
├ ● /[categorySlug]                        160 B          69.4 kB
├   ├ /food
├   └ /materials
└ ● /[categorySlug]/[productId]            161 B          69.4 kB
    ├ /food/1
    ├ /food/2
    ├ /materials/1
    └ /materials/2
+ First Load JS shared by all              69.3 kB
  ├ chunks/17-07b769614d6c9bc3.js          66.4 kB
  ├ chunks/main-app-f7991bdf5a7d528d.js    253 B
  └ chunks/webpack-d2d5d8dda7543a35.js     2.63 kB

and if I also add the generateStaticParams to [categorySlug]/page.tsx:

export async function generateStaticParams() {
  return [{
    categorySlug: 'overriden',
  }]
}

the output will be:

Route (app)                                  Size     First Load JS
┌ ○ /                                        0 B                0 B
├ ● /[categorySlug]                          160 B          69.4 kB
├   └ /overriden
└ ● /[categorySlug]/[productId]              161 B          69.4 kB
    ├ /food/1
    ├ /food/2
    ├ /materials/1
    └ /materials/2
+ First Load JS shared by all                69.3 kB
  ├ chunks/17-07b769614d6c9bc3.js            66.4 kB
  ├ chunks/main-app-f7991bdf5a7d528d.js      253 B
  └ chunks/webpack-d2d5d8dda7543a35.js       2.63 kB

arelenas avatar Dec 06 '22 18:12 arelenas

Is this a flawed implementation of the bottom up approach? When I console.log(params) I am getting the params for every post.

export async function generateStaticParams() {
  const paths = await getAllPostsSlugs();

  function loopParams(posts: Post[]) {
    const params: { courseSlug: string; sectionSlug: string; postSlug: string }[] = [];

    posts.forEach((post) => {
      const postSlug = post?.slug;
      post?.sections?.forEach((section) => {
        const sectionSlug = section?.slug;
        section?.courses?.forEach((course) => {
          const courseSlug = course?.slug;

          params.push({ courseSlug, sectionSlug, postSlug });
        });
      });
    });

    return params;
  }

  return loopParams(paths);
}

Let's say I have a post 1 and 2. If I visit /course/section/1, I get:

{
  courseSlug: 'course',
  sectionSlug: 'section',
  postSlug: '1'
}
{
  courseSlug: 'course',
  sectionSlug: 'section',
  postSlug: '2'
}

Instead of just:

{
  courseSlug: 'course',
  sectionSlug: 'section',
  postSlug: '1'
}

trevorpfiz avatar Dec 10 '22 13:12 trevorpfiz

@ClemannD There is a typo in the documentation that suggest generateStaticParams receives a params argument. It actually recieves an object with a params property.

https://github.com/ClemannD/next-13-test-repo/blob/main/app/%5BcategorySlug%5D/%5BproductId%5D/page.tsx#L10

this should be

export async function generateStaticParams({ params: { categorySlug }}: any) {

Additionally the reason no params are passed in for the given setup has to do with what Layouts and Pages are in scope for a given static generation pass.

For /app/[categorySlug]/[productId]/page.tsx the only layout.js in the ancestor path is the root /app/layout.js and this does not define a generateStaticParams nor could it because there are no params at that route level.

The reason the generateStaticParams found in /app/[categorySlug]/page.tsx aren't used is this page is not part of the layout hierarchy for /app/[categorySlug]/[productId]/page.tsx.

If you created a /app/[categorySlug]/layout.tsx and moved your generateStaticParams from the page.tsx to it then params would be successfully passed into the deeper generateStaticParams for productId.

You can use an empty Layout export default function ({ children }) { return children } if you don't actually have any shared layout content to render.

However if you did not want to go with that approach you can just output every combination of categorySlug and productId from the generateStaticParams you have already defined

gnoff avatar Dec 14 '22 20:12 gnoff

I was able to reproduce the top down approach having issues: CodeSandbox or Repo.

leerob avatar Jan 26 '23 05:01 leerob

Slightly related: I am looking for types for 3 things:

  1. Some App-folder equivalent for: const Page = ({ someProp }: InferGetStaticPropsType<typeof getStaticProps>

  2. A NextPage replacement than works with next build.

Im sure its on your roadmap, but are they already in 13.1.7-canary.14 ?

nvegater avatar Feb 19 '23 19:02 nvegater

@ClemannD There is a typo in the documentation that suggest generateStaticParams receives a params argument. It actually recieves an object with a params property.

https://github.com/ClemannD/next-13-test-repo/blob/main/app/%5BcategorySlug%5D/%5BproductId%5D/page.tsx#L10

this should be

export async function generateStaticParams({ params: { categorySlug }}: any) {

Additionally the reason no params are passed in for the given setup has to do with what Layouts and Pages are in scope for a given static generation pass.

For /app/[categorySlug]/[productId]/page.tsx the only layout.js in the ancestor path is the root /app/layout.js and this does not define a generateStaticParams nor could it because there are no params at that route level.

The reason the generateStaticParams found in /app/[categorySlug]/page.tsx aren't used is this page is not part of the layout hierarchy for /app/[categorySlug]/[productId]/page.tsx.

If you created a /app/[categorySlug]/layout.tsx and moved your generateStaticParams from the page.tsx to it then params would be successfully passed into the deeper generateStaticParams for productId.

You can use an empty Layout export default function ({ children }) { return children } if you don't actually have any shared layout content to render.

However if you did not want to go with that approach you can just output every combination of categorySlug and productId from the generateStaticParams you have already defined

This works, thank you! The parent generateStaticParams is returning a promise to the child level generateStaticParams, therefore an 'any' type will solve this problem.

stevenlyd avatar Mar 02 '23 14:03 stevenlyd

Hi, this has been updated in v13.2.4-canary.7 of Next.js, please update and give it a try!

ijjk avatar Mar 08 '23 20:03 ijjk

Hi, this has been updated in v13.2.4-canary.7 of Next.js, please update and give it a try!

Tried this, but still after update, my params object in getStaticParams at app/lessons/[lessonId]/[sessionId]/page.tsx is empty, even though I output slugs at app/lessons/[lessonId]/page.tsx

arnelamo avatar Mar 18 '23 00:03 arnelamo

@arnelamo can you provide a minimal repo with this issue?

ijjk avatar Mar 20 '23 22:03 ijjk

This closed issue has been automatically locked because it had no new activity for a month. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

github-actions[bot] avatar Apr 20 '23 00:04 github-actions[bot]