When I try to use the auto-generated Breadcrumb URL, the server returns a 404.
1)
npx create-payload-appwith PostgreSQL
2) Default seed
3) Add
poststo nestedDocsPlugin collections
4) Setup a parent -> child -> grandchild relationship between existing
postsThe Breadcrumbs field automatically generates what appears to be the correct url, i.e.
/<parent>/<child>/<grandchild>when looking at the post that is the grandchild. Using that URL results in a 404. Using just the
/<grandchild>segment loads the grandchild post.
I have tried going into
src/app/(frontend)/posts/[slug]/page.tsxand change the
whereproperty of the
payload.findfunction call inside the
queryPostBySlugfunction to also check to see if a breadcrumb with a URL like the slug exists. However, this section of code is never hit when trying to use one of the nested Breadcrumb URLs.
My next thought is that I might need to change how a post auto-generates its slug? But wouldn't that also change how the Breadcrumb generates its URLs and break that part? I'm kinda lost at what to do next.
Oh, I have also tried similar with the default
pagescollection to the same effect.
Your route [slug] only catches single segments like /posts/grandchild, not nested paths like /posts/parent/child/grandchild.
Solution: Use a catch-all route with [...slug]:
Rename folder:
mv src/app/(frontend)/posts/[slug] src/app/(frontend)/posts/[...slug]Update page.tsx:
// src/app/(frontend)/posts/[...slug]/page.tsx
interface PageProps {
params: {
slug: string[] // Now an array
}
}
async function queryPostBySlug(slugSegments: string[]) {
const payload = await getPayload({ config })
const fullPath = '/' + slugSegments.join('/')
const posts = await payload.find({
collection: 'posts',
where: {
'breadcrumbs.url': {
equals: fullPath,
},
},
limit: 1,
})
return posts.docs[0] || null
}
export default async function PostPage({ params }: PageProps) {
const post = await queryPostBySlug(params.slug)
if (!post) {
notFound()
}
return (
<article>
<h1>{post.title}</h1>
{/* Your content */}
</article>
)
}Optional - Support both nested and direct URLs:
async function queryPostBySlug(slugSegments: string[]) {
const payload = await getPayload({ config })
if (slugSegments.length === 1) {
// Check both slug and breadcrumb for single segment
const posts = await payload.find({
collection: 'posts',
where: {
or: [
{ slug: { equals: slugSegments[0] } },
{ 'breadcrumbs.url': { equals: '/' + slugSegments[0] } }
],
},
limit: 1,
})
return posts.docs[0] || null
}
// For nested paths, only check breadcrumb
const fullPath = '/' + slugSegments.join('/')
const posts = await payload.find({
collection: 'posts',
where: { 'breadcrumbs.url': { equals: fullPath } },
limit: 1,
})
return posts.docs[0] || null
}So here's how I ended up solving for this. I'm using multi-tenant as well so that's taken into consideration as well. This is in progress and I've had to step away from it for a few days so if it doesn't work, I'll ping back when I work on it again.
const { isEnabled: draft } = await draftMode()
const params = await paramsPromise
console.log({ params })
let slug: string | undefined = undefined
let breadCrumb: string | undefined = undefined
if (params?.slug && Array.isArray(params.slug)) {
breadCrumb = `/${params.slug.join('/')}`
slug = params.slug[params.slug.length - 1]
} else if (typeof params?.slug === 'string') {
breadCrumb = `/${params.slug}`
slug = params.slug
}
const headers = await getHeaders()
const subdomain = headers.get('host')?.split('.')[0] || null
const payload = await getPayload({ config: configPromise })
const { user } = await payload.auth({ headers })
console.log({ slug })
const slugConstraint: Where = slug
? {
slug: {
equals: slug,
},
'breadcrumbs.url': {
equals: breadCrumb,
},
}
: {
or: [
{
slug: {
equals: '',
},
},
{
slug: {
equals: 'home',
},
},
{
slug: {
exists: false,
},
},
],
}
const pageQuery = await payload.find({
collection: 'pages',
overrideAccess: true,
user,
draft,
where: {
and: [
{
'tenant.domain': {
equals: subdomain,
},
},
slugConstraint,
],
},
})
console.log({ pageQuery })
const pageData = pageQuery.docs?.[0]This is also for the root [...slug] route and not the nested posts specifically, but same should apply.
ohhh, i didn't know the naming of the directory could have an effect like that! i think that is the missing piece. will let you know if i stumble and can't pick myself up when trying this. Thanks!
yup, this post helped me get it working! the only additional thing i had to do was to modify my
generateStaticParamsto return an array of url segments from the breadcrumbs url; by default, this function only returns the slug of the document
export async function generateStaticParams() {
const payload = await getPayload({ config: configPromise })
const posts = await payload.find({
collection: 'posts',
draft: false,
limit: 1000,
overrideAccess: false,
pagination: false,
select: {
slug: true,
breadcrumbs: {
url: true,
},
},
})
const params = posts.docs.map(({ slug, breadcrumbs }) => {
if(breadcrumbs && breadcrumbs.length > 0 && breadcrumbs[0].url) {
return { slug: breadcrumbs[0].url.split('/') }
}
return { slug: [slug] }
})
return params
}Perfect! Yeah, generateStaticParams needs to match the route structure. Thanks for posting the complete solution!
Just heads up - split('/') on a URL like /parent/child will give you an empty string as the first element. You might want to add .filter(Boolean) or .slice(1) after the split. But if it's working, Next.js might be handling it fine.
yeah, i actually originally wrote my code assuming that behavior (because i tested it in a dev console in my browser, first). turns out, that assumption broke my code because Next.js or Node (not sure which) removed the empty string element
Nice debugging!
did anyone else run into issues with their link component only rendering the slug but not the parents or grandparents when navigating?
i noticed various issues with how i was doing things before when i started up a new project, so my fixes are below. i simplified
queryTopicBySlugand modified
generateStaticParamsbecause i learned that the last breadcrumb is always the one with the full path.
i also changed the
whereclause from
orto
andto ensure that i am getting the page with a matching slug and a breadcrumb matching the full path. i noticed that i was sometimes getting inconsistent matching results and this was why. originally, the
'breadcrumbs.url': { : fullPath, },part could match to any record in the collection that has the full path of the desired page. this mean that it could select a child of the current page. so, i made it match both the shortest slug of the current page and the full path in the breadcrumb to help guard against multiple records with the same slug.
import React, { cache } from 'react'
import { LivePreviewListener } from '@/components/LivePreviewListener'
import PageClient from './page.client'
import configPromise from '@payload-config'
import { draftMode } from 'next/headers'
import { getPayload } from 'payload'
export async function generateStaticParams() {
const payload = await getPayload({ config: configPromise })
const posts = await payload.find({
collection: 'topics',
draft: false,
depth: 1,
limit: 1000,
overrideAccess: false,
pagination: false,
select: {
slug: true,
breadcrumbs: {
url: true,
},
},
})
const params = posts.docs.map(({ slug, breadcrumbs }) => {
if (breadcrumbs && breadcrumbs.length > 1) {
return { slug: breadcrumbs.pop()?.url?.split('/').filter(Boolean) }
}
return { slug: [slug] }
})
return params
}
type Args = {
params: Promise<{
slug?: string[]
}>
}
export default async function Topic({ params: paramsPromise }: Args) {
const { isEnabled: draft } = await draftMode()
const { slug = [''] } = await paramsPromise
const topic = await queryTopicBySlug({ slug })
return (
<article>
<PageClient />
{draft && <LivePreviewListener />}
<h2>{topic.title}</h2>
</article>
)
}
const queryTopicBySlug = cache(async ({ slug }: { slug: string[] }) => {
const { isEnabled: draft } = await draftMode()
const payload = await getPayload({ config: configPromise })
const fullPath = '/' + slug.join('/')
const result = await payload.find({
collection: 'topics',
draft,
limit: 1,
overrideAccess: draft,
pagination: false,
where: {
and: [
{
slug: {
equals: slug.pop(),
},
},
{
'breadcrumbs.url': {
equals: fullPath,
},
},
],
},
})
return result.docs?.[0] || null
})This package is helpful
https://github.com/akhrarovsaid/payload-nested-pages-exampleand some examples to use Folders for breadcrumbs?
@127418710519578624@654031862146007055
. I will search for it by messages, but mb you know some
If I can't find an example with folders, I'll try to find some time this week to make an example of using folders instead of nested-docs 👍 good shout
i heard about this, but cant find nothig, so thank you! For now, fill try your repo with nesteddocs
Star
Discord
online
Get dedicated engineering support directly from the Payload team.