I encountered a similar issue in NextJS 13 using the App router in a catch-all route segment (folder named [...slug]
)and the generateStaticParams
function, which has a return signature similar to what you're showing here for getStaticPaths
. I was trying to dynamically generate blog post slug paths based on a specific folder's contents, including subdirectories. Here is what I discovered and how I solved it.
The Problem
The next build
and next dev
commands handle processing static parameters differently in some meaningful way. next dev
returns the expected path including the slashes when getting the filenames using globSync
:
export async function generateStaticParams() {
const filenames = globSync(
[postsDirectory + '/**/*.md', postsDirectory + '/**/*.mdx'],
{ absolute: false, cwd: postsDirectory },
)
const markdownRegex = /\.md(x)?$/
return filenames.map((filename) => ({
slug: [filename.replace(markdownRegex, '')],
}))
}
generateStaticParams
in dev returns an array of parameters with slugs like these:
[
{ slug: [ 'article2' ] },
{ slug: [ 'article1' ] },
{ slug: [ '2023/newest-article' ] },
{ slug: [ '2023/directory/some-other-article' ] }
]
which are accessed via code like this:
export default async function ArticleBySlug({
params,
}: {
params: { slug: string[] }
}) {
const filePath = path.join(postsDirectory, params.slug[0])
// Do something with filePath to get article contents and build page...
However, when this code ran in next build
, the resulting array looks more like this:
[
{ slug: [ 'article2' ] },
{ slug: [ 'article1' ] },
{ slug: [ '2023%2Fnewest-article' ] },
{ slug: [ '2023%2Fdirectory%2Fsome-other-article' ] }
]
Obviously, these filenames can't be found, and the build process crashes.
The Solution
The answer was to make use of the fact that the slug
parameter is expecting an array. Instead of returning an array of one string with slashes (filename.replace(markdownRegex, '')
), we can return an actual array of route segments. The code now looks like this:
export async function generateStaticParams() {
const filenames = globSync(
[postsDirectory + '/**/*.md', postsDirectory + '/**/*.mdx'],
{ absolute: false, cwd: postsDirectory },
)
const markdownRegex = /\.md(x)?$/
return filenames.map((filename) => ({
slug: filename.replace(markdownRegex, '').split(path.sep)
}))
}
Now instead of returning an array containing one path string from which we remove the file extension, we are returning the results of using string.split
on that path, splitting on path.sep
which will be the operating system appropriate separator. This results in an array of segments in the slug
property for each parameter, such as:
[
{ slug: [ 'article2' ] },
{ slug: [ 'article1' ] },
{ slug: [ '2023', 'newest-article' ] },
{ slug: [ '2023', 'directory', 'some-other-article' ] }
]
Then, instead of joining the string directly, we can use the spread operator to expand the array into path.join
:
export default async function ArticleBySlug({
params,
}: {
params: { slug: string[] }
}) {
const filePath = path.join(postsDirectory, ...params.slug)
// Do something with filePath to get article contents and build page...
This pattern works equally well in next build
and next dev
(using NextJS 13 and the app router). I assume if you are returning an array of strings as the slug
parameter, it should work in the pages router with getStaticPaths
as well.