Skip to main content

Overview

Poke-Nex leverages Static Site Generation (SSG) combined with Incremental Static Regeneration (ISR) to deliver instant page loads and SEO-ready content while keeping data fresh. The application pre-renders 1025+ Pokémon pages at build time, achieving exceptional performance metrics.
Build Performance: 1030+ static pages generated in < 12 seconds using multi-threaded worker pools.

Static Site Generation (SSG)

Pre-rendering All Pokémon Pages

Every Pokémon detail page is statically generated at build time using the generateStaticParams function. This ensures instant page loads with zero API calls at runtime.
src/app/pokemon/[slug]/page.tsx
export async function generateStaticParams() {
  const { data, error } = await getPokemonList()
  if (error) throw new Error(JSON.stringify(error))
  return data.map((pokemon) => ({ slug: pokemon.name }))
}
How it works:
  1. At build time, generateStaticParams fetches the complete list of 1025+ Pokémon
  2. Next.js generates a static HTML page for each Pokémon slug
  3. All pages are deployed as pre-rendered HTML files
  4. Users get instant page loads with full SEO metadata

Dynamic Metadata Generation

Each static page includes dynamically generated metadata for optimal SEO:
export async function generateMetadata({ params }: Props) {
  const { slug } = await params
  const { data, error } = await getPokemonDetail(slug)
  if (!data || error) return { title: 'Pokémon not found' }
  return {
    title: `${data.name.charAt(0).toUpperCase() + data.name.slice(1)} | Pokénex Pro`,
    description: `Details, types, and statistics of ${data.name}.`,
    openGraph: {
      images: [data.assets.home.default || ''],
    },
  }
}

Incremental Static Regeneration (ISR)

Revalidation Strategy

Poke-Nex implements a weekly ISR cycle (604,800 seconds) to keep data fresh without sacrificing performance:
src/lib/api/pokemon.api.ts
export const fetchPokemonByID = async (
  slug: string,
  extended = false
): Promise<ApiPokemonResponse> => {
  const baseResponse = await fetch(`${BASE_URL}/pokemon/${slug}`, {
    next: { revalidate: 604800 },
  })
  // ...
}
Revalidation periods:
  • Pokemon detail pages: 604,800 seconds (7 days)
  • Pokemon list (GraphQL): 604,800 seconds (7 days)
  • Variety data: 604,800 seconds (7 days)
The 7-day revalidation window is ideal for Pokémon data, which rarely changes. This balances data freshness with optimal performance.

How ISR Works in Poke-Nex

  1. Initial Build: All 1030+ pages are generated statically
  2. Serving Static Pages: Users receive instant HTML responses from the CDN
  3. Background Revalidation: After 7 days, the next request triggers background regeneration
  4. Stale-While-Revalidate: Users continue seeing the cached version while the page regenerates
  5. Updated Cache: New requests receive the freshly regenerated page

Service Layer Architecture

Clean Architecture Pattern

Poke-Nex implements a clean architecture with separate layers for API calls, data adaptation, and business logic:
src/services/pokemon.service.ts
export const getPokemonDetail = async (
  slug: string,
  extended: boolean = true
): Promise<ServiceResponse<PokemonDetail>> => {
  try {
    if (!slug) throw new Error('The Pokémon slug or ID is required.')
    const pokemonData = await fetchPokemonByID(slug, extended)
    if (!pokemonData) throw new ApiError('Pokémon data is null')
    return { data: adaptPokemon(pokemonData), error: null }
  } catch (error) {
    const fault = handleServiceError(error, '[getPokemonDetail]')
    return {
      data: null,
      error: fault,
    }
  }
}
Architecture layers:
  1. Fetchers (pokemon.api.ts): Raw API calls with ISR configuration
  2. Adapters (pokemon-detail.adapter.ts): Transform API responses to application models
  3. Services (pokemon.service.ts): Business logic and error handling
  4. Pages: Consume services for rendering

GraphQL for Optimized List Fetching

The home page uses GraphQL to fetch only the necessary fields for the Pokémon list:
src/lib/api/pokemon.api.ts
export const fetchPokemonListGQL = async (
  limit: number = Number(LIMIT)
): Promise<GQLPokemonSummaryList> => {
  const query = `
    query getPokemonList($limit: Int, $offset: Int) {
      pokemon(limit: $limit, offset: $offset) {
        id
        name
        pokemontypes {
          type {
            name
          }
        }
      }
    }
  `
  const response = await fetch(GQL_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query,
      variables: { limit, offset: 0 },
    }),
    next: { revalidate: 604800 },
  })
  // ...
}
GraphQL fetching reduces payload size by requesting only id, name, and types for list views, improving build time and initial page load.

Performance Metrics

Build Time Performance

  • Total Pages: 1030+ (1025 Pokémon + app pages)
  • Build Time: < 12 seconds
  • Parallel Processing: Multi-threaded worker pools
  • Average Time per Page: ~0.011 seconds

Runtime Performance

  • LCP (Largest Contentful Paint): Instant (pre-rendered HTML)
  • TTI (Time to Interactive): < 1 second
  • API Calls at Runtime: 0 (fully static)
  • CDN Cache Hit Rate: ~100%
While ISR provides automatic revalidation, be mindful of your API rate limits. Poke-Nex’s 7-day cycle ensures minimal API usage while keeping data fresh.

Best Practices

1. Use Extended Fetch Selectively

The extended parameter in fetchPokemonByID controls whether to fetch additional data:
// Full detail page with varieties, evolution, etc.
const pokemonData = await fetchPokemonByID(slug, extended: true)

// List view without extra data
const pokemonData = await fetchPokemonByID(slug, extended: false)

2. Configure Appropriate Revalidation Periods

  • Static data (Pokémon stats): 7 days or more
  • User-generated content: 1 hour or less
  • Frequently updated data: Consider on-demand revalidation

3. Error Handling for Build Failures

Always handle errors gracefully to prevent build failures:
export async function generateStaticParams() {
  const { data, error } = await getPokemonList()
  if (error) throw new Error(JSON.stringify(error))
  return data.map((pokemon) => ({ slug: pokemon.name }))
}

4. Implement Proper SEO Metadata

Generate unique metadata for each static page:
  • Title: Include Pokémon name and site name
  • Description: Concise summary of page content
  • Open Graph: Include high-quality images
  • JSON-LD: Structured data for search engines

Build docs developers (and LLMs) love