Fetch data with getByUID using two root types in NextJS

I am trying to get my head around an issue of having two custom root document types (page and article) and also fetching the data correctly. I understand how to use a sub directory for a different document type and it works fine, but the following I cannot get working.

I have the following document types and routing:

  {
  	type: "home",
  	path: "/",
  },
  {
  	type: "page",
  	path: "/:uid",
  },
  {
  	type: "article",
  	path: "/:uid",
  },
  {
  	type: "work",
  	path: "/work/:uid",
  }

As I understood it from documentation my folder structure looks like this:

src
β”œβ”€β”€ app
β”‚   β”œβ”€β”€ [uid]
β”‚   β”‚   └── page.tsx
β”‚   β”œβ”€β”€ work
β”‚   β”‚   └── [uid]
β”‚   β”‚       └── page.tsx
β”‚   β”œβ”€β”€ layout.tsx
β”‚   └── page.tsx
└── prismicio.ts

In my page.tsx I have the following code:

const client = createClient()
const page = await client.getByUID('page', params.uid).catch(() => notFound())

My question is, how do I include the article type in this fetching of data, it seems like getByUID only takes string for a single document type, so how can I include the article type in that query without hitting not found and a 404 page?

I tried the following in hope of checking if there was data and then use for the document returned, but it generates an error if the document type is not the type viewed by the user.

const page = await client.getByUID('page', params.uid).catch()
const article = await client.getByUID('article', params.uid).catch()

or:

const page = await (client.getByUID('page', params.uid).catch() || client.getByUID('article', params.uid).catch())

Plus, I've tried multiple other ideas, but nothing seems to work as soon as an article page is viewed. The reason I have two different root type documents is because of content and layout which is different, but the page always lives in the root hierarchy of the website.

Please give me some hints here on how to solve this.

Best,
F

What are going for your generateStaticParams? The solution may lie in that function

1 Like

Sorry @nf_mastroianni , I am not sure I understand. Can you please elaborate a bit around your answer?

I am trying to render my two different page types Page and Article, and they both are located in the root of the website as you can see above in my routing table.

I tried to add an article.tsx document under [uid], but that did not work.

β”œβ”€β”€ app
β”‚   β”œβ”€β”€ [uid]
β”‚   β”‚   β”œβ”€β”€ page.tsx
β”‚   β”‚   └── article.tsx
β”‚   β”œβ”€β”€ layout.tsx
β”‚   └── page.tsx
└── prismicio.ts

I am new to node.js but it seems that all documents rendering a page, is named page.tsx. That leaves me with one single document under [uid] which should be able to handle data from two different page types.

That is where I got stuck.
How can I do that or what is incorrect with my setup?

Hello @Fiskbulle ,

It appears you’re using the app router. There are reserved file names. Your article.tsx will not work. page.tsx will render your page/article content. You just need to make sure to get all pages and all articles. Then you iterate through them in your generateStaticParams to make sure a page is created for each page/article.

export async function generateStaticParams() {
  const client = createClient()
  const pages = await client.getAllByType('page')
  const articles = await client.getAllByType('article')
  const pages_to_make = […pages, …articles]

  return pages_to_make.map(page => {
    return { uid: page.uid }
  })
}

That’s what I’m thinking so far.

Thanks @nf_mastroianni
That looks like something that could work, I'll try it.

Hi @nf_mastroianni

I tried your idea and it works for the static params as you perfectly demonstrated, and my page is now rendering content from my Article type.

For my other function which will render the page content, it generates error due to the await function and the time it takes to fetch and update the data. I am also lacking a notFound() function if the page do not exist.

/**
 * Rendering layout and data for page
 */

type Params = { uid: string };

export default async function Page({ params }: { params: Params }) {

    const client = createClient()

    const pages = await client.getAllByType('page')
    const articles = await client.getAllByType('article')
    
    const content = [...pages, ...articles]

    let page

    content.map(temp => { 
        
        if (temp.uid === params.uid) {

            page = temp
        }
    })
    
    return (
        <main className="grow">
            <article>

                <Hero data={page.data} />
                <SliceZone slices={page.data.slices} components={components} />

            </article>
        </main>
    )
}

Not sure what I am doing incorrect here, but my code doesn't look good and I cannot say i am confident with my current effort, knowing that something else will go wrong when someone enters a non-existing URL.

Help is definitely appreciated

HI @Fiskbulle ,

I don't think you want to lean on the getAllByType method for your page component. I think it might be wise to add a little something in our generateStaticParams so you know the content type. Let's revisit that:

export async function generateStaticParams() {
  const client = createClient()
  const pages = await client.getAllByType('page')
  const articles = await client.getAllByType('article')
  const pages_to_make = […pages, …articles]

  return pages_to_make.map(page => {
    return { uid: page.uid, type: page.type }
  })
}

Now that type will be something that is passed to the page component, you can check for it and use the getByUID method which I believe is more efficient that getByAll methods.

/**
 * Rendering layout and data for page
 */

// type Params = { uid: string }; updated type below
type Params = {
  uid: string
  type: 'page' | 'article'
}

export default async function Page({ params }: { params: Params }) {

    const client = createClient()

    let page 
    if(params.type === 'page') {
      page = await client.getByUID('page', params.uid)
    } else {
      page = await client.getByUID('article', params.uid)
    }
    
    return (
        <main className="grow">
            <article>

                <Hero data={page.data} />
                <SliceZone slices={page.data.slices} components={components} />

            </article>
        </main>
    )
}

How does this work for ya?

@nf_mastroianni Great! Your code looks like an excellent and neat solution.

However, I cannot get the generateStaticParams() function to be updated with the new variables, so I am still running into issues in the page function of not finding anyone of the two page types declared in the if statement?

The Page() function seems only to include the params.uid variable and the params.type variable do not even exist. Perhaps there is a cache for those static params or I am not sure how this can be that difficult.

I cannot be the only person in the whole world trying to use two page types in the root of my website. It is strange that this is not explained anywhere :thinking:

To make it clear why I would like to use two different page types in the root is the following, i.e. each page will have different design and layout, and the custom content slices will determine what a content editor can and cannot do.

Type A

  • Slice A
  • Slice B
  • Slice C

Type B

  • Slice A
  • Slice D

Both page types live in the root of the website and they might have URL aliases as /type-a-page-one, /type-a-page-two, /type-b-page-three, and etc.

This is also why the routes look like the following:

const routes: prismic.ClientConfig["routes"] = [
  {
  	type: "home",
  	path: "/",
  },
  {
  	type: "type a",
  	path: "/:uid",
  },
  {
  	type: "type b",
  	path: "/:uid",
  }
];

You’re not able to add props to your params as seen above?

When you console.log params, what do you get?

Hi again @nf_mastroianni, and thanks for all help and effort.

Your code looks correct to me and I can clearly see what we are trying to do, but when I am running it as intended, it only returns params.uid. The console only returns the UID of the active page.

I even tried to add a random string to see what is going on (as below), but even that is ignored.

return { uid: page.uid, test: 'wagga', type: page.type }

I am new to Next.js and I've earlier used React to build a website with an identical page structure and routing without any issues. However, the server side rendering wasn't working as I wanted it in React and I am now translating everything I have into Next.js.

The console returns the following, both in my Page as in my Meta function.

params { uid: 'test-page' }

I found someone having a similar issue, and if I understood that correctly, for the variable to be saved in the generateStaticParams() function, it must first be declared in the route resolver. I am not familiar with how that can be done when the page type is in the root of the website URL though.

In the documentation of Next.js , there are several examples, but non that directly suits what I am trying to achieve. Anyone who can guide me in the correct direction to organize folders and pages based on what I've described above with two page types in the root?

You’re correct and I’m wrong. I thought you could pass additional props through this function. It appears that’s not the case. Wishful thinking on my part.

1 Like

I think we’ll figure this out. Perhaps we can make use of a conditional. I’m AFK at the moment. I’ll check on this later today.

Ok, here's what I've come up with. It works, I don't know if it's "the best way."

export default async function Page({ params }: { params: Params }) {
  const client = createClient()
  let page
  page = await client.getByUID('page', params.uid).catch(() => null)

  if (!page) {
    console.log('NO PAGE, MUST BE ARTICLE')
    page = await client.getByUID('article', params.uid).catch(() => notFound())
  }

  return (
    <div>
      <PrismicRichText field={page.data.title} />
      <SliceZone slices={page.data.slices} components={components} />
    </div>
  )
}

export async function generateMetadata({
  params,
}: {
  params: Params
}): Promise<Metadata> {
  const client = createClient()
  let page
  page = await client.getByUID('page', params.uid).catch(() => null)

  if (!page) {
    console.log('NO PAGE, MUST BE ARTICLE')
    page = await client.getByUID('article', params.uid).catch(() => notFound())
  }

  return {
    title: page.data.meta_title,
    description: page.data.meta_description,
  }
}

export async function generateStaticParams() {
  const client = createClient()
  const pages = await client.getAllByType('page')
  const articles = await client.getAllByType('article')
  const content = [...pages, ...articles]

  return content.map((page) => {
    return { uid: page.uid }
  })
}

Notice the generateMetadata has to have the same "trick." Here's my routes:

const routes: prismic.ClientConfig['routes'] = [
  // Examples:
  // {
  // 	type: "homepage",
  // 	path: "/",
  // },
  {
    type: 'page',
    path: '/:uid',
  },
]

image

@nf_mastroianni Thanks for helping out. I'll try that :slight_smile:

I've just realized that this would be easy to achieve if the getByUID function would take an array ot page types with the help of what you suggested to merge the two page types into the params.uid variable in the generateStaticParams function.

const client = createClient()
const page = await client.getByUID(['page', 'article'], params.uid).catch(() => notFound())

Perhaps that is something that @angeloashmore might want to weigh in on. I thought maybe there was a way to use the client.get() method with some filtering to get content from multiple types and then filter by UID. That does not seem to be possible based on my amateur understanding. For now, does my prior "solution" work for you?

Hi @nf_mastroianni

Sorry for a late answer. Yes, it works nicely now and I think your updated suggestion was the best option for this scenario. Now we only need to wait for Prismic to update the getByUID function to support an array of page types :smile:

Here's the final document:

/**
 * Page layout
 * 
 */

import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { createClient } from '@/prismicio'
import { SliceZone } from '@prismicio/react'
import { components } from '@/slices'

/**
 * Loading local app layout and components
 */

import Hero from '@/components/Hero'

/**
 * Fetching meta data and open graph content
 */

export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {

    let page

    const client = createClient()
    const settings = await client.getSingle('settings')

    page = await client.getByUID('page', params.uid).catch(() => null)

    if (page === null) {

        page = await client.getByUID('article', params.uid).catch(() => notFound())
    }
    
    return {

        title: page.data.meta_title || settings.data.meta_title,
        description: page.data.meta_description || settings.data.meta_description,
        openGraph: {
        images: [page.data.meta_image.url || ''],
        },
    }
}

/**
 * Rendering static content for page
 */

export async function generateStaticParams() {

    const client = createClient()

    const pages = await client.getAllByType('page')
    const articles = await client.getAllByType('article')

    const content = [...pages, ...articles]
    
    return content.map((page) => {
                
        return { uid: page.uid }
    })
}

/**
 * Rendering layout and data for page
 */

type Params = { uid: string }

export default async function Page({ params }: { params: Params }) {
    
    let page

    const client = createClient()

    page = await client.getByUID('page', params.uid).catch(() => null)

    if (page === null) {

        page = await client.getByUID('article', params.uid).catch(() => notFound())
    }

    return (
        <main className="grow">
            <article>

                <Hero data={page.data} />
                <SliceZone slices={page.data.slices} components={components} />

            </article>
        </main>
    )
}

Here's the page routes for it:

const routes: prismic.ClientConfig["routes"] = [
  {
  	type: "home",
  	path: "/",
  },
  {
  	type: "page",
  	path: "/:uid",
  },
  {
  	type: "article",
  	path: "/:uid",
  },
];

Thanks for helping out. Well appreciated.

1 Like