#Fetching Contentful data with App directory

141 messages · Page 1 of 1 (latest)

reef obsidian
#

Hey everyone, I'm relatively new to Next.js and currently learning my ways around the app directory and the features.

Right now I am not sure how to implement contentful and map the items. Here's how I am doing it right now and I realized that getStaticProps is not available in the app router. Any help would be very much appreciated.

Here is the code that I have:


export async function getStaticProps() {

  const client = createClient({
    space: process.env.CONTENTFUL_SPACE_ID,
    accessToken: process.env.CONTENTFUL_ACCESS_KEY, 
  })

  const res = await client.getEntries({
    content_type: 'resourcesPage'
  })

  return {
    props: {
      resources: res.items
    }
  }
}
delicate mangoBOT
#

🔎 This post has been indexed in our web forum and will be seen by search engines so other users can find it outside Discord

🕵️ Your user profile is private by default and won't be visible to users outside Discord, if you want to be visible in the web forum you can add the "Public Profile" role in id:customize

✅ You can mark a message as the answer for your post with Right click -> Apps -> Mark Solution
(if you don't see the option, try refreshing Discord with Ctrl + R)

round oasis
#

or if only a certain component needs the contentful data, fetch it directly within that component instead of at the page level

#

With the app directory you can really make sure that only the components that need the data fetch/access it.

reef obsidian
#

@round oasis Awesome this is really helpful, thanks so much! I'll give it a try galactic_brain

reef obsidian
#

Thanks so much man here's the solution that made it work for anyone who's also may have the same problem as me:

in page.jsx/tsx


async function fetchContentful() {
  const client = createClient({
    space: process.env.CONTENTFUL_SPACE_ID,
    accessToken: process.env.CONTENTFUL_ACCESS_KEY, 
  })

  const res = await client.getEntries({
    content_type: 'resourcesPage'
  })

  return res.items
}

export default async function Home() {
const resources = await fetchContentful()
  console.log(resources);

  return (
    <main>{...}</main>
  )
};```
round oasis
#

Seen this asked a lot so no worries kekw

reef obsidian
#

@round oasis Hey man im back with another question hahaha

#

how would you approach adding category filtering in your application? For instance I have a list of cards that are dynamically mapped from contentful and I would like to filter them by category using the useSearchParams to store the query in the url

e.g https://website.com/category?=tools

#

I'm still quite confused on whether if I should fetch the data in the Card component or the page.jsx

#

Here's my code so far:

My page.jsx

  const client = createClient({
    space: process.env.CONTENTFUL_SPACE_ID,
    accessToken: process.env.CONTENTFUL_ACCESS_KEY,
  });

  const res = await client.getEntries({
    content_type: "resourcesPage",
  });

  return res.items;
}

export default async function Home() {
  const resources = await fetchContentful();


  return (
    <main>
      <div className="resource-card">
          {resources.map((resource) => {
            return <ResourceCard key=                {resource.sys.id} resource={resource} />;
          })}
       </div>
     </main>

My card component (where i just take in the props passed from the page.jsx):

export default function ResourceCard({ resource }) {
  const { title, category, thumbnail, tags } = resource.fields;

  // Extract tag names from the tags reference field
  const tagNames = tags.map((tag) => tag.fields.tag);

  tagNames.sort()

  return (
    <Link href="/">{...}</Link>
round oasis
#

With the app directory the mental model for data fetching has changed since you have such granular control over it, in the pages directory you had to do it at the page level if you wanted to fetch on the server, with the app directory you can do it at the component level meaning you should move data fetching to the Server Components that directly consume that data.

#

For your example what I would do is extract that Div that wraps the map and move all that logic into a ResourceContainer or ResourceGrid (depending on how you render the cards) component fetch the data in there map the items in there and then wrap the entire container in a suspense boundary in your page

reef obsidian
#

Thanks so much man, this is really helpful. Now i understand a bit more clear.

#

Right now im trying to implement a filtering functionality that query the state of the category chosen by the users up to the Url using useSearchParams. From what I researched, I need to use the "use client" directive to use useSearchParams. So following your suggestion, would it be appropriate if I map the data in the ResourceCard component itself as a server component and then implement the filtering functionality in the ResourceContainer which would be a client component?

#

Once again thanks so much, im learning quite a bit in terms of nextjs client and server components haha

round oasis
# reef obsidian Right now im trying to implement a filtering functionality that query the state ...

Yeah this is an option, it's actually what I did orginially when I was using useSearchParams, I will say for my use case I decided to use Dynamic Routes instead of searchParams for the purpose of pre-rendering the first page of each category using generateStaticParams, then anything after that pre-render will be dynamically rendered. The problem I had personally with searchParams are it forces your entire route to be dynamically rendered which I did not want for the sake of querying the API on every request, especially since the contentful client doesn't use the fetch API so there is no request memoization, once unstable_cache becomes stable this won't be that big of a problem since I can cache the results and revalidate the data with a tag, but, until then this is what I'm doing and honestly I find it much cleaner than searchParams and I gain all the beneifts of putting state in the URL just as route params instead of searchParams.

#

That being said, you can wrap the ResourceContainer in a Suspense boundary so that the useSearchParams hook doesn't make the entire route client-side rendered, but, you're going to have to read the searchParams from the page regardless so that'll make the route dynamic, but, it'll be Server-Side rendered.

round oasis
reef obsidian
#

@round oasis thanks man, ill try to abstract out my code in a different file to play around with this. Will keep you in the loop if you don't mind haha

reef obsidian
#

@round oasis hahah awesome

#

im back with an update

#

I think I'm getting a bit closer in terms of implementing the filtering stuff

#
  const client = createClient({
    space: process.env.CONTENTFUL_SPACE_ID,
    accessToken: process.env.CONTENTFUL_ACCESS_KEY,
  });

  const res = await client.getEntries({
    content_type: "resourcesPage",
    include: 2,
  });

  return res.items;
}

export default async function Home() {
  const resources = await fetchContentful();

   const categoryCount = {};
  // Iterate over resources and update categoryCount
  resources.forEach((resource) => {
    const category = resource.fields.category.fields.category;
    categoryCount[category] = (categoryCount[category] || 0) + 1;
  });

  const router = useRouter();
  const selectedCategory = router.query.category || "";

  // Filter resources based on the selected category
  const filteredResources = selectedCategory
    ? resources.filter((resource) => resource.fields.category.fields.category === selectedCategory)
    : resources;

  return (
    <main className="my-20">
      <section>
        <div className="flex mb-8 justify-center">
          {/* Filtering button, mapping the categories via contentful */}
          {Object.entries(categoryCount).map(([category, count]) => (
            <Link
              key={category}
              href={`/?category=${encodeURIComponent(category)}`}
              className={`${
                selectedCategory === category
                  ? "border-text"
                  : "border-dim-gray"
              }`}
            >
              <span className="text-sm">{category}</span>
              <span className="text-xxs">{count}</span>
            </Link>
          ))}
        </div>
        <div className="">
          {filteredResources.map((resource) => (
            <ResourceCard key={resource.sys.id} resource={resource} />
          ))}
        </div>
      </section>
    </main>
  );
}
#

Right now i've abstracted my code so I can just fully focus down on the functionality part

#

so the home page here would technically be the ResourceContainer component

#

right now I am trying to filter the categories by using the useRouter() hook and push up the query onto the url depending on which category is selected

#

but the problem is that useRouter is only available in client component meanwhile the curernt component is a server component

#

in this case how do I make sure that I can use useRouter without clashing with the server components stuff?

#

would love your guidance here. Once again, thanks so much for helping me out haha

round oasis
# reef obsidian in this case how do I make sure that I can use useRouter without clashing with t...

You're only using useRouter for reading the searchParams here it looks like:

  1. You can get the searchParams in a page server component from props
  2. If this was a client component you shouldn't use useRouter to read query params, instead you should use useSearchParams. Your way wouldn't work even if you imported from next/router (the pages router version of the useRouter hook) since it's not compatiable with the app router model.

If the Home component in this case would become the ResourceContainer like you're saying then I would suggest grabbing the searchParams from the parent page and then passing the searchParams down to the ResourceContainer.

reef obsidian
#

ohhh okay that definitely makes sense, ill definitely give it a try

#

so in the page.jsx it would be something like

#
import ResourceContainer from "@/components/Card/ResourceContainer";
import { useSearchParams } from "next/navigation";



export default function Home() {
  const searchParams = useSearchParams()

  const category =     searchParams.get('category')

  return (
    <main>
     
     <ResourceContainer category=    {category} />
    </main>
  );
}```
round oasis
#

close, and yeah that would work but now you're making the page a client component when it doesn't need to be, it can stay a server component since pages recieve searchParams as a prop.

It would like this:

#
import ResourceContainer from "@/components/Card/ResourceContainer";


export default function Home({ searchParams }) {
  const category = searchParams.get('category')

  return (
    <main> 
     <ResourceContainer category={ category } />
    </main>
  );
}
#

Sorry it would actually look like this:

import ResourceContainer from "@/components/Card/ResourceContainer";


export default function Home({ searchParams }) {
  const { category } = searchParams

  return (
    <main> 
     <ResourceContainer category={ category } />
    </main>
  );
}
#

you have to either destructure the param or just access it like searchParams.category since it returns a plain JS object and not a URLSearchParams instance.

#

I prefer to destructure, but, it's personally preference.

reef obsidian
#

ahhh I see

#

all these stuff are destroying my brain haha

#

so then when passing the category down to the ResourceContainer, would I use the useRouter to read the searchParams passed from the page.js?

round oasis
round oasis
#

export async function ResourceContainer({ category }) {}

reef obsidian
#

yess alright, it starts to click a bit now

#

thank you so much

#

ill have a play around with the code

round oasis
#

Yeah accessing the searchParams is Next.js everything after is just plain ol' React

reef obsidian
#

hahahah awesome man

#

how long did it take you to adapt to the app directory?

round oasis
# reef obsidian how long did it take you to adapt to the app directory?

Well, I went from plain React to the Next.js pages router and after about a month in the pages router, app router became attractive enough for me to try in some side projects and then migrating my main application over.

I guess it depends how you define adapt, it took me about a month to have a good enough understanding to feel comfortable using it (as in anything I could previously I knew how to do in app router), but, they are still things I am learning today about it. I've been using App Router since around Febuary of this year.

#

I'd say the trickest part of the app router for me currently (and probably for most) is the cache invalidation in Next.js. Other than that I feel pretty confident.

reef obsidian
#

Mmm yea that's super cool

#

I just moved to Next.js from plain React just like a week ago or so

round oasis
#

Yeah I think moving from plain React to Next.js App Router overall is an easier transition than moving from plain React to Next.js Pages Router.

The app router has less Next.js specific abstractions and more of improving/extending the core React features which I absolutely love.

#

Of course the React Server Component paradigm shift is massive though and definitely a lot to re-consider

reef obsidian
#

Yes I totally agree haha, A lot of Next.js features are really awesome, especially the SEO features + performance capabilities

#

yep stepping into the server component stuff is super fresh for me

round oasis
#

It is for everyone, Server Components have only been stable for a few months

#

Yeah Next.js is in a really good spot right now, and the improvements they've announced are coming like PPR (Partial Pre-Rendering) and an easier way to interact with the caching behaviors in Next.js with unstable_cache & unstable_noStore coming will help as well. I don't have a single complaint about Next besides how aggressive the caching is (which is good in most cases) but the invalidation of that cache isn't very intuitive, but, as I said those above functions will help out with that.

umbral ice
#

Hi @reef obsidian @round oasis
I also like the Next.js. Next.js is the best framework among javascript library I think.
It can implement SSR and PPR and also ISR. There are several ways to render in Next.js project.
And since released Next.js@13, they can support App routing also. It is innovative thing as well.

reef obsidian
round oasis
reef obsidian
#

Haha yes, ill be on the grind

#

@round oasis also, I am back with where we left off. Might be a redundant question but, once I accept the category prop in the ResourceContainer, what can I do with it? It's somewhat difficult for me to wrap my head around the params stuff. Should I dynamically render the content stuff in its own component so that I can use useSearchParams() in the ResourceContainer as a client component and then pass down the variable down to the Filtering button component?

#
  const resources = await fetchContentful();

  const categoryCount = {};
  // Iterate over resources and update categoryCount
  resources.forEach((resource) => {
    const categoryItem = resource.fields.category.fields.category;
    categoryCount[categoryItem] = (categoryCount[categoryItem] || 0) + 1;
  });

  //   Filter resources based on the selected category
  const filteredResources = selectedCategory
    ? resources.filter(
        (resource) =>
          resource.fields.category.fields.category === selectedCategory
      )
    : resources;

  return (
    <section>
      <div>
        {/* Filtering button, mapping the buttons with contentful */}
    {Object.entries(categoryCount).map(([category, count]) => {
          return (
            <Link
              key={category}
              href={`/?category=${categories}`}
            >
              <span>{category}</span>
              <span>{count}</span>
            </Link>
          );
        })}
      </div>

      <div>
      
{filteredResources.map((resource) => (
          <ResourceCard key={resource.sys.id} resource={resource} />
        ))}
      </div>
    </section>
  );
}```
#

Note: I changed the prop name to categories instead of category since it clashes with the category parameter in the mapped function

round oasis
#

for example:

if the URL is https://website.com/?category=latest

then const { category } = searchParams category in this case will be equal to "latest"

reef obsidian
#

ahh I see

#

I'm still a bit lost haha, in plain react I remember having to use useSearchParams to get the query and use setSearchParams setter function to set the search params

#

would I need to import the useSearchParams in the ResourceContainer component to get the query?

#

I also took a look at the documentation on updating useSearchParams as well but not too sure what's going on 🥲. it seems that they're doing this in a client component to implement the sorting

reef obsidian
round oasis
round oasis
#

I achieve the same type of filtering by grabbing the category and page (I'm using pagination) from the params, validating them, and passing them to a contentful fetch query which displays the filtered data.

reef obsidian
#

ohhh I see

reef obsidian
round oasis
#

I'll show you mine, bear in mind that I'm using Dynamic Route params instead of searchParams for caching/pre-rendering reasons, but, pretend that params in this case are searchParams it would work the same way.

export default async function CategoryPage({ params }: Props) {
  const { categories, category, page, totalPages } = await validateNewsRoutes(params.category, params.page)
  const skip = page <= 1 ? 0 : (NEWS_LIMIT * page) - NEWS_LIMIT

  return (
    <>
      <NewsSelector category={ category } categories={ categories } currentPage={ page } />
      <Suspense fallback={<NewsLoader />}>
        <NewsGrid category={ category } limit={ NEWS_LIMIT } skip={ skip } />
      </Suspense>
      <NewsButtons totalPages={ totalPages } category={ category } currentPage={ page } />
    </>
  )
}
#

also I'm using TypeScript so ignore any non JS stuff lol

#

I'm taking the params from the page, just like you would, in my case I'm passing them to a validate function that makes sure the params are things I can work with and if not assigning them to things I can work with.

Then I pass that category and other things to my NewsGrid component, where I make the contentful fetch using that category and map through the Posts in there.

#
export default async function NewsGrid({ category, limit, skip }: Props) {
  const newsPosts = await getPosts<TypeNewsSkeleton>({
    content_type: 'news',
    order: ['-fields.date'],
    'fields.category.sys.contentType.sys.id': 'newsCategory',
    'fields.category.fields.slug[match]': category === 'latest' ? null : category,
    skip: skip,
    limit: limit
  })
  
  return (
    <section className='grid grid-cols-news w-4/5 mx-auto justify-center items-center gap-12 px-4 pb-4'>
      { newsPosts.items.map(post => (
        <NewsCard key={ post.sys.id } post={ post } />
      ))}
    </section>
  )
}
#

also getPosts in this case is my custom wrapper around client.getEntries() so that I can get TypeSafe queries and re-use around my application as needed.

reef obsidian
#

This is very much helpful again, i guess for my case i wouldnt need to validate my params right?

#

Also, just out of curiosity how did you also implement your NewsSelector as well?

round oasis
#
export default function NewsSelector({ category, categories, currentPage }: Props) {
  const router = useRouter()
  const sortedOptions = ['latest', ...categories].filter(option => option !== category)

  const onChangeHandler = (option: string) => {
    categories.forEach(category => {
      if (category === option) return
      else router.push(`/${NEWS_ROUTE}/${option}/${currentPage}`)
    })
  }

  return (
    <section className='relative top-0 -left-2 w-full md:max-w-[65.2%] mx-auto mt-8 z-10 h-20 p-4 animate-fadeIn font-orbitron'>
      <Select onValueChange={ value => onChangeHandler(value.toLowerCase()) }>
        <SelectTrigger className='capitalize max-w-[180px] bg-primary text-secondary border-secondary font-bold'>
          <SelectValue placeholder={ category.replace('-', ' ') } />
        </SelectTrigger>
        <SelectContent className='capitalize bg-primary border-secondary'>
          <SelectGroup>
            { sortedOptions.map((option, index) => (
              <SelectItem key={ `${option}_${index}` } value={ option } className='text-gray-100 focus:bg-secondary font-medium'>
                { option.replace('-', ' ') }
              </SelectItem>
            ))}
          </SelectGroup>
        </SelectContent>
      </Select>
    </section>
  )
}
#

All I'm doing in my NewsSelector is creating an array of categories that do not include the currently selected one, maping over those and displaying them in a select box, and the currently selected category is the current value of the select box, and when someone clicks a new category I use router.push() (remember to import this route from next/navigation and not next/router) to set the send the user to the "new" route that I can pull category and page out of the URL to update my fetch. In your case, you would be pushing with searchParams like so /?category=YOUR_VALUE_HERE

reef obsidian
#

Yes 👍 it would be /?category=VALUE

reef obsidian
#

@round oasis I think i'm sooo close to getting the feature implemented all thanks to you man

#

just final stretch and I might need your support again haha

#

right now I got the filtering tabs to work and push the queries in the URL

#

but i'm not too sure how to filter and display the cards accordingly to the category that is selected with contentful and such

#

Here is my ResourceContainer.jsx

import { createClient } from "contentful";

// Components
import ResourceCard from "@/components/Card/ResourceCard";
import Tab from "../TabNavigation/Tab";

async function fetchContentful() {
  const client = createClient({
    space: process.env.CONTENTFUL_SPACE_ID,
    accessToken: process.env.CONTENTFUL_ACCESS_KEY,
  });

  const res = await client.getEntries({
    content_type: "resourcesPage",
    include: 2,
    order: "fields.category.sys.id"
  });

  return res.items;
}

export default async function ResourceContainer({ category }) {
  const resources = await fetchContentful();

  return (
    <section>
      <Tab resources={resources} />
      <div className="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
        {resources.map((resource) => (
          <ResourceCard key={resource.sys.id} resource={resource} />
        ))}
      </div>
    </section>
  );
}
#

to update you with the new change: I have extracted my filtering buttons into a component so I can pass in the contentful categories data in the ResourceContainer and use client routing features in the Tab component

#

I'm not too sure but I noticed that you used the order key in the contentful to filter the card accordingly as well thinq

round oasis
round oasis
reef obsidian
#

hmmm would it help if I were to show the json for the content type?

round oasis
reef obsidian
#

Yep I have a separate content type for the categories and then referencing the categories in the resource content

round oasis
#

Specifically these two lines:

'fields.category.sys.contentType.sys.id': 'newsCategory',
    'fields.category.fields.slug[match]': category === 'latest' ? null : category,
#

'newsCategory' is whatever the ID of your category content type is and the string under that is a simple matcher.

reef obsidian
#

ohhh okay, i'll play around with this

#

thanks man!

#

absolute goat man galactic_brain

#

it's finally working

#

one last problem right now haha every time I filter the category the category buttons are also filtered

#

hmmm

round oasis
#

let me see your filter buttons component that you made

reef obsidian
#

here you go:

"use client"

// Note: This component is used to filter resources by category
import { useRouter } from "next/navigation";


export default function TabButtons({resources}) {
  const router = useRouter();

  const categoryCount = {};
  // Iterate over resources and update categoryCount
  resources.forEach((resource) => {
    const categoryItem = resource.fields.category.fields.category;
    categoryCount[categoryItem] = (categoryCount[categoryItem] || 0) + 1;
  });

  const categories = Object.keys(categoryCount);

  const onChangeHandler = (e) => {
    categories.forEach((category) => {
      if (e.target.innerText === category) {
        router.push(`/?category=${category}`, {scroll: false});
      }
    })
  }

  return (
    <div className="flex mb-8 justify-center">
      {/* Filtering button */}
      {Object.entries(categoryCount).map(([item, count]) => {
        return (
          <button
            key={item}
            onClick={(e) => onChangeHandler(e)}
            className=" py-1 px-4 flex gap-x-1 font-medium border border-dim-gray rounded-full hover:border-text transition-all active:text-dark-charcoal active:bg-accent"
          >
            <span className=" text-sm ">{item}</span>
            <span className="flex justify-center items-center text-text text-xxs w-4 h-4 bg-super-dark-gray rounded-full leading-none">
              {count}
            </span>
          </button>
        );
      })}
    </div>
  );
}
#

I think it makes sense since i'm taking the resources data from the ResourceContainer so every time I filter the buttons will be filtered as well

round oasis
#

Yeah if the behavior is expected then seems fine, can you give me an example of whats happening and what you expect?

Cause if you're clicking on say "latest" and then the filtering buttons change and remove "latest" as one of the options, then that means you're coding it like a select/combo box instead of a just filter buttons.

reef obsidian
#

yep for sure

#

Here's the filtering right now

#

Whereas it would be ideal to have all the filtering buttons stay when a category is selected

round oasis
#

Where are you filtering the resources, in the parent component of the ResourceContainer?

reef obsidian
#

I'm currently filtering it in the ResourceContainer

round oasis
reef obsidian
#

I just updated the order of the contentful to:

const res = await client.getEntries({
    content_type: "resourcesPage",
    include: 2,
    order: ['-fields.publishedDate'],
    'fields.category.sys.contentType.sys.id': "categories",
    'fields.category.fields.category': category === 'all' ? null : category,
  });
round oasis
#

Okay yup, so the category data that gets passed to the Tab component needs to be an independent fetch from the filtering. You'll need two data fetches, one to get the categories, and one to filter the resources by the current category. It's unfortunately a neccessary thing (I hate it), but, it's the only way to get the correct behavior, and it won't be that big of a deal once unstable_cache becomes stable.

#

I'd suggest lifting the wrapping <section> tag and Tab component up to the parent, fetching the categories in there, passing that data to the Tab component, and then wrapping the ResourceContainer in a suspense boundary, and making the filtering fetch inside there, that way you have the correct data that each component needs.

#

Essesntially, your page will look like this:

export async function Page({ searchParams }){
const { category } = searchParams
const categories = fetchContentfulCateogries()
return (
  <whatever tags you have here>
    <section>
      <Tab cateogories={ categories } />
      <Suspense fallback={<Loading />}>
        <ResourceContainer />
       </Suspense>
    </section>
  </whatever tags you have here>
)
}
#

this is just like a visual example, you can fill in whatever your page needs.

round oasis
reef obsidian
#

ohhhh I see

round oasis
#

except my fetch happens inside that validateNewsRoute function since I need to validate my params before using them for data fetches.

reef obsidian
#

yep makes sense

#

wow thank you so much man, I'm beyond words on your dedication to teach me all these stuff

round oasis
reef obsidian
#

Yea it's amazing, I can totally resonate with reciprocating on what we've learned. you're definitely one of the great ones in the community for sure. I don't think I have seen a thread this long 😂

#

is it alright if I send you a friend request? I'd love to show you the final application once it's finished

round oasis
#

Yeah I accepted it. I'd love to see the final product for sure.