Dave.Q
Dave.Q

Reputation: 399

Next JS build isn't building out every path

Summary / Issue

I've created an anime database app using Nextjs with deployment on Vercel. The build was fine and the initial page rendered, but only a few of my dynamic routes are being rendered, the rest display a 404 page. I went into the deploy log and found that for each dynamic page, only 10 routes were being built for every dynmaic route.

Deploy Screenshot from Vercel

enter image description here

While working in development (localhost:3000), there were no issues and everything ran fine.

The routes are based on the id of each title and there are thousands of titles.

My Code

Here is my code for one of the pages using getStaticPaths and getStaticProps

export const getStaticProps = async ({ params }) => {
  const [anime, animeCharacters, categories, streaming, reviews] = await Promise.all([
    fetch(`https://kitsu.io/api/edge/anime/${params.id}`),
    fetch(`https://kitsu.io/api/edge/anime/${params.id}/characters`),
    fetch(`https://kitsu.io/api/edge/anime/${params.id}/categories`),
    fetch(`https://kitsu.io/api/edge/anime/${params.id}/streaming-links`),
    fetch(`https://kitsu.io/api/edge/anime/${params.id}/reviews`),
  ])
    .then((responses) =>
      Promise.all(responses.map((response) => response.json()))
    )
    .catch((e) => console.log(e, "There was an error retrieving the data"))

  return { props: { anime, animeCharacters, categories, streaming, reviews } }
}

export const getStaticPaths = async () => {
  const res = await fetch("https://kitsu.io/api/edge/anime")
  const anime = await res.json()

  const paths = anime.data.map((show) => ({
    params: { id: show.id },
  }))

  return { paths, fallback: false }
}

[id] is my dynamic route and as you can see, it's only being populated with 10 routes (the first 3 and 7 additional).

Despite the number of shows, I'm looping over each show and grabbing its ID and then passing that as the path.

What I've thought of

The API I'm using is the Kitsu API.

Within the docs, it states: "Resources are paginated in groups of 10 by default and can be increased to a maximum of 20". I figured this might be why 10 paths are being generated, but if that was the case, then why would it work fine in production and in deployment? Also, when I click on each poster image, it should bring me to that specific title by its id, whihc is dynamic, so it shouldn't matter how many recourses are being generated initially.

Code for dynamic page `/anime/[id]

import { useState } from "react"
import { useRouter } from 'next/router'
import fetch from "isomorphic-unfetch"
import formatedDates from "./../../helpers/formatDates"

import Navbar from "../../components/Navbar"
import TrailerVideo from "../../components/TrailerVideo"
import Characters from "./../../components/Characters"
import Categories from "../../components/Categories"
import Streamers from "../../components/Streamers"
import Reviews from "../../components/Reviews"

const Post = ({ anime, animeCharacters, categories, streaming, reviews}) => {
  const [readMore, setReadMore] = useState(false)

  const handleReadMore = () => setReadMore((prevState) => !prevState)

  let {
    titles: { en, ja_jp },
    synopsis,
    startDate,
    endDate,
    ageRating,
    ageRatingGuide,
    averageRating,
    episodeCount,
    posterImage: { small },
    coverImage,
    youtubeVideoId,
  } = anime.data.attributes

  const defaultImg = "/cover-img-default.jpg"

  const synopsisSubString = () =>
    !readMore ? synopsis.substring(0, 240) : synopsis.substring(0, 2000)

  const router = useRouter()
  if(router.isFallback) return <div>loading...</div>

  return (
    <div className='relative'>
      <div className='z-0'>
        <img
          className='absolute mb-4 h-12 min-h-230 w-full object-cover opacity-50'
          src={!coverImage ? defaultImg : coverImage.large}
        />
      </div>
      <div className='relative container z-50'>
        <Navbar />

        <div className='mt-16 flex flex-wrap md:flex-no-wrap'>
          {/* Main  */}
          <div className='md:max-w-284'>
            <img className='z-50 mb-6' src={small} />

            <div className='xl:text-lg pb-6'>
              <h1 className='mb-2'>Anime Details</h1>
              <ul>
                <li>
                  <span className='font-bold'>Japanese Title:</span> {ja_jp}
                </li>
                <li>
                  <span className='font-bold'>Aired:</span>{" "}
                  {formatedDates(startDate, endDate)}
                </li>
                <li>
                  <span className='font-bold'>Rating:</span> {ageRating} /{" "}
                  {ageRatingGuide}
                </li>
                <li>
                  <span className='font-bold'>Episodes:</span> {episodeCount}
                </li>
              </ul>
            </div>

            <Streamers streaming={streaming} />
          </div>

          {/* Info Section */}
          <div className='flex flex-wrap lg:flex-no-wrap md:flex-1 '>
            <div className='mt-6 md:mt-40 md:ml-6 lg:mr-10'>
              <h1 className='sm:text-3xl pb-1'>{en}</h1>
              <h2 className='sm:text-xl lg:text-2xl pb-4 text-yellow-600'>
                {averageRating}{" "}
                <span className='text-white text-base lg:text-lg'>
                  Community Rating
                </span>
              </h2>
              <div>
                <p className='max-w-2xl pb-3 overflow-hidden xl:text-lg'>
                  {synopsisSubString()}
                  <span className={!readMore ? "inline" : "hidden"}>...</span>
                </p>
                <button
                  className='text-teal-500 hover:text-teal-900 transition ease-in-out duration-500 focus:outline-none focus:shadow-outline'
                  onClick={handleReadMore}
                >
                  {!readMore ? "Read More" : "Read Less"}
                </button>
              </div>
              <Categories categories={categories} />
              <Reviews reviews={reviews}/>
            </div>

            {/* Sidebar */}
            <section className='lg:max-w-sm mt-10 md:ml-6 lg:ml-0'>
              <TrailerVideo youtubeVideoId={youtubeVideoId} />
              <Characters animeCharacters={animeCharacters} />
            </section>
          </div>
        </div>
      </div>
    </div>
  )
}

export const getStaticProps = async ({ params }) => {
  const [anime, animeCharacters, categories, streaming, reviews] = await Promise.all([
    fetch(`https://kitsu.io/api/edge/anime/${params.id}`),
    fetch(`https://kitsu.io/api/edge/anime/${params.id}/characters`),
    fetch(`https://kitsu.io/api/edge/anime/${params.id}/categories`),
    fetch(`https://kitsu.io/api/edge/anime/${params.id}/streaming-links`),
    fetch(`https://kitsu.io/api/edge/anime/${params.id}/reviews`),
  ])
    .then((responses) =>
      Promise.all(responses.map((response) => response.json()))
    )
    .catch((e) => console.log(e, "There was an error retrieving the data"))

  return { props: { anime, animeCharacters, categories, streaming, reviews } }
}

export const getStaticPaths = async () => {
  const res = await fetch("https://kitsu.io/api/edge/anime")
  const anime = await res.json()

  const paths = anime.data.map((show) => ({
    params: { id: show.id },
  }))

  return { paths, fallback: true }
}

export default Post

Screenshot of Errror

enter image description here

Repo

Upvotes: 2

Views: 4561

Answers (1)

subashMahapatra
subashMahapatra

Reputation: 6837

If the API you are working with serves resources in groups of 10, then when you call the API in getStaticPaths you only have 10 id ahead of time. Using static generation in nextjs builds static pages for all the available ids ahead of time in production mode. But when in development mode your server will recreate each page on per request basis. So to solve this problem you can build the first 10 pages and make the rest of the pages to be fallback. Here is how you do it.

export const getStaticPaths = async () => {
  const res = await fetch("https://kitsu.io/api/edge/anime")
  const anime = await res.json()

  // you can make a series of calls to the API requesting 
  // the next page to get the desired amount of data (100 or 1000)
  // how many ever static pages you want to build ahead of time

  const paths = anime.data.map((show) => ({
    params: { id: show.id },
  }))

  // this will generate 10(resource limit if you make 1 call because your API returns only 10 resources) 
  // pages ahead of time  and rest of the pages will be fallback
  return { paths, fallback: true }
}

Keep in mind when using {fallback: true} in getStaticPaths you need have some sort of loading indicator because the page will be statically generated when you make a request for the first time which will take some time(usually very fast).

In your page that you want to statically generate

function MyPage = (props) {

  const router = useRouter()

  if (router.isFallback) {
    // your loading indicator
    return <div>loading...</div>
  }

  return (
    // the normal logic for your page
  )
}

P.S. I forgot to mention how to handle errors where API responds with 404 or 500 and the resource doesn't exist to send as props when using fallback in static generation.

So here's how to do it.

const getStaticProps = async () => {
  // your data fetching logic

  // if fail
  return {
    props: {data: null, error: true, statusCode: 'the-status-code-you-want-to-send-(500 or 404)'} 
  }  

  // if success 
  return {
    props: {data: 'my-fetched-data', error: false} 
  }

}

// in the page component
import ErrorPage from 'next/error';

function MyStaticPage(props) {
  if (props.error) {
   return <ErrorPage statusCode={404}/>
  }

  // else you normal page logic
}

Let me know if it helped or you encountered some error while implementing. Here is where you can learn more https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation

Upvotes: 3

Related Questions