You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js/TypeScript中generateStaticParams()失效问题求助

Next.js 动态路由静态预渲染失效及500错误修复

问题现象

  • 执行npm run build时仅生成3个静态页面(实际应生成所有产品对应的详情页)
  • 部署到Vercel后,点击产品卡片跳转详情页触发500内部服务器错误
  • 功能此前正常,新增TypeScript类型定义后出现异常

相关代码排查

动态路由页面(app/services/[id]/page.tsx)

import Image from "next/image"

export async function generateStaticParams() {
    const res = await fetch('https://dummyjson.com/products')
    const products = await res.json()

    const paths = products.products.map((product: {id: number}) => ({
        id: `${product.id}`,
    }))

    return paths
}

async function getProduct(id: number) {
    const res = await fetch(`https://dummyjson.com/products/${id}`)
    const data = await res.json()
    return data
}

export default async function Service({ params }: { params: {id: number} }) {
    const product = await getProduct(params.id)

    return (
        <div style={{display: "flex", flexDirection: "column"}}>
            <h1>{product.title}</h1>
            <Image src={product.thumbnail} alt="asd" width={400} height={400}/>
            <span>In stock: {product.stock}</span>
        </div>
    )
}

父页面(app/services/page.tsx)

"use client";

import '@/styles/BlogServices.css'
import ServiceSection from '@/components/ServiceSection.tsx'
import React from 'react'

export default function Services() {
    interface Product {
        id: number,
        title: string,
        description: string,
        price: number,
        image: string, // 注意:dummyjson返回的图片字段是thumbnail,不是image
        category: string
    }

    const [searchedService, setSearchedService] = React.useState<Product[]>([])
    const [defaultService, setDefaultService] = React.useState<Product[]>([])

    React.useEffect(() => {
        fetch('https://dummyjson.com/products')
        .then(res => res.json())
        .then(res => {
            setSearchedService(res.products)
            setDefaultService(res.products)
        })
    },[])

    function handleSearch (value:string) {
        setSearchedService(defaultService.filter(item => item.title.toLowerCase().includes(value.toLowerCase())))
    }

    return (
        <div className="services-page-container">
            <div className="blog-service-page-title-container parent-flex-column-center dark:bg-slate-900">
                <span className="blog-service-page-short-title">SERVICES</span>
                <h1 className="blog-service-page-title font-bold">Choose and book service with us</h1>
                <p className="blog-service-page-desc text-page-subtitle text-xl dark:text-slate-400">Subscribe to learn about new product features, the latest in technology, solutions, and updates.</p>
            </div>
            <ServiceSection 
                servicePage
                descStyle='services-section-desc'
                handleSearch={handleSearch}
                searchedService={searchedService}
                defaultService={defaultService}
            />
        </div>
    )
}

产品卡片组件(components/ServiceCard.tsx)

"use client";

import servicearrow from '../public/assets/servicearrow.svg'
import '../styles/Card.css'
import Image, { StaticImageData } from 'next/image'
import { useRouter } from 'next/navigation'

interface ServiceCard {
    id?: number;
    title: string;
    desc: string;
    date?: string;
    img: string | StaticImageData;
    imgStyle: string;
    contStyle: string;
    descStyle: string;
    servicepage?: boolean;
    handleClick?: () => void;
}

export default function ServiceCard(props: ServiceCard) {
    const router = useRouter()

    function handleClick() {
        props.servicepage && router.push(`/services/${props.id}`)
    }
    
    return (
        <div className={`card-container ${props.contStyle} dark:bg-slate-600`} onClick={handleClick}>
            <Image src={props.img} className={`card-img ${props.imgStyle}`} width={300} height={300} alt=''/>
            <span className='card-date'>{props.date}</span>
            <h3 className='text-xl font-bold mb-5 dark:text-slate-200'>{props.title}</h3>
            <p className={`card-description ${props.descStyle} dark:text-slate-50`}>{props.desc}</p>
            <span className='text-service-card-orange flex mt-auto dark:text-orange-700'>View full information <Image className='ml-6' src={servicearrow} alt="" /></span>
        </div>
    )
}

问题根源

  1. 路由参数类型不匹配:Next.js路由参数始终是字符串类型,但动态路由页面中把params.id定义为number,同时getProduct函数接收number类型参数,导致类型冲突,静态生成时无法正确遍历所有路径,运行时也会因类型转换异常触发500错误。
  2. 接口字段定义错误:父页面Product接口中定义的image字段与dummyjson返回的thumbnail字段不符,可能导致卡片图片加载异常,间接影响路由跳转逻辑。
  3. 可选参数未做校验:ServiceCard中id是可选属性,跳转时未判断id是否存在,可能生成无效路由。

修复步骤

1. 修正动态路由页面的类型定义

import Image from "next/image"

// 定义明确的产品类型
interface Product {
  id: number;
  title: string;
  thumbnail: string;
  stock: number;
}

export async function generateStaticParams() {
    // 增加错误处理,避免fetch失败导致生成空路径
    try {
        const res = await fetch('https://dummyjson.com/products')
        if (!res.ok) throw new Error('Failed to fetch product list')
        
        const data = await res.json()
        const paths = data.products.map((product: Product) => ({
            id: product.id.toString(),
        }))

        return paths
    } catch (err) {
        console.error('Error generating static params:', err)
        return []
    }
}

// 修改getProduct参数为string,匹配路由参数类型
async function getProduct(id: string) {
    const res = await fetch(`https://dummyjson.com/products/${id}`)
    const data = await res.json()
    return data as Product
}

// 修正params类型为string
export default async function Service({ params }: { params: {id: string} }) {
    const product = await getProduct(params.id)

    return (
        <div style={{display: "flex", flexDirection: "column"}}>
            <h1>{product.title}</h1>
            <Image src={product.thumbnail} alt={product.title} width={400} height={400}/>
            <span>In stock: {product.stock}</span>
        </div>
    )
}

2. 修正父页面的Product接口

// 修正字段名,匹配dummyjson返回结构
interface Product {
    id: number;
    title: string;
    description: string;
    price: number;
    thumbnail: string; // 替换image为thumbnail
    category: string;
}

3. 完善卡片组件的路由跳转逻辑

function handleClick() {
    // 增加id存在性校验,避免无效跳转
    if (props.servicepage && props.id !== undefined) {
        router.push(`/services/${props.id}`)
    }
}

4. 验证修复效果

  • 执行npm run build,查看控制台输出的静态页面数量是否与产品总数一致
  • 本地启动生产环境服务(npm start),测试跳转是否正常
  • 重新部署到Vercel,确认500错误消失

内容的提问来源于stack exchange,提问作者Ucha

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 15:45:56