Next.js 14构建时预渲染错误排查求助
Next.js 14 构建预渲染失败问题排查与解决
问题描述
运行next build时构建失败,触发预渲染错误:
页面“/”预渲染出错
页面“/categories”预渲染出错
导出时以下路径出现错误:
/_not-found
/categories/page: /categories
/competition/page: /competition
/page: /
已查阅官方文档但未解决,需分析错误原因、修复方法,以及Next.js 14页面预渲染的正确配置步骤。
项目目录结构
src/app/ /page.tsx /cateogies/page.tsx // 注:原目录拼写错误,应为categories
首页代码(app/page.tsx)
"use client"; import BackgroundPhotoFooter from "@/elements/BackgroundPhoto"; import CategoryList from "@/elements/CategoryList"; import Footer from "@/elements/footer"; import { useEffect, useState } from "react"; import { Modal } from "antd/lib"; import { getCategories, getCompetitions } from "@/services/categories"; import { useTranslation } from "next-i18next"; import LoginModule from "@/elements/Login"; import { ICategory } from "@/models/Category"; import { Spin } from "antd"; import { ICompetition } from "@/models/Competition"; import CompetitionList from "@/elements/CompetitionList"; import { Box, Container, Grid, Typography } from "@mui/material"; import ButtonCustom from "@/elements/ButtonCustom"; import Link from "next/link"; import Menu from "@/elements/Menu"; import Header from "@/elements/header"; import Image from "next/image"; import { useKeycloak } from "@/context/KeycloakContext"; export default function Home() { const { t } = useTranslation("description"); const [categories, setCategories] = useState<ICategory[]>([]); const [competitions, setCompetition] = useState<ICompetition[]>([]); const [isLoading, setLoading] = useState(true); const [isCategoryLoading, setCategoryLoading] = useState(true); const [isModalOpen, setIsModalOpen] = useState(false); const [selectedCategory, setSelectedCategory] = useState(0); const { authenticated, loading: authLoading } = useKeycloak(); const onCategorySelect = (id: number) => { setSelectedCategory(id); setCategoryLoading(true); getCompetitions() .then((data) => setCompetition(data)) .catch((err) => console.error(err)); setTimeout(() => { setCategoryLoading(false); }, 1000); }; const handleOk = () => { setIsModalOpen(false); }; const handleCancel = () => { setIsModalOpen(false); }; useEffect(() => { if (isLoading) { try { getCategories() .then((data) => { setCategories(data); setSelectedCategory(data[0].id); }) .catch((err) => console.error(err)); getCompetitions() .then((data) => setCompetition(data)) .catch((err) => console.error(err)); } catch (err) { console.error("errore: " + err); } setTimeout(() => { setLoading(false); setCategoryLoading(false); }, 1000); } }, [isLoading]); const exploreAll = (categoryId: number) => { console.log("Explore category: " + categoryId); }; if (authLoading) { return ( <div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "100vh", }} > <Spin size="large" /> </div> ); } return ( <> <Header /> <Modal title="" open={isModalOpen} onOk={handleOk} onCancel={handleCancel} footer={null} closable={false} okButtonProps={{ style: { display: "none" } }} cancelButtonProps={{ style: { display: "none" } }} > <div className="columns-2 ..."> <div> <Image loading="lazy" quality={75} style={{ marginBottom: "30%" }} src="/dog.png" width={200} height={700} alt="dog" /> </div> <div> <Image loading="lazy" quality={75} src="/ui.png" width={240} height={60} alt="photo image" style={{ marginLeft: "20px", marginTop: "20px" }} /> </div> </div> </Modal> <Box style={{ backgroundImage: `url(${"header.png"})`, width: "100%", height: "900px", backgroundPosition: "center", backgroundSize: "cover", backgroundRepeat: "no-repeat", }} > <Container maxWidth="xl"> <Grid container spacing={6} sx={{ pt: 10, }} > <Grid item xs={12} md={6}> <Box component="img" sx={{ mt: 10, mb: 5, height: "auto", width: 300, }} alt="IShotIt" src="/logo.png" /> <Typography variant="h1" sx={{ color: "white", mb: 4 }}> Show The World Your Best Photos. </Typography> <Typography variant="h2" sx={{ color: "white" }}> Recognized Awards and Big Prizes! </Typography> </Grid> <Grid item xs={12} md={6} sx={{ display: { xs: "none", md: "inline-block" }, }} > {authenticated ? <Menu invert={false} /> : <LoginModule />} </Grid> </Grid> </Container> </Box> {isLoading ? ( <div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "50vh", }} > <Spin size="large" /> </div> ) : ( <> {categories != undefined && categories.length > 0 && ( <CategoryList categories={categories} onCategorySelect={onCategorySelect} /> )} {isCategoryLoading ? ( <div style={{ display: "flex", justifyContent: "center", alignItems: "center", height: "50vh", }} > <Spin size="large" /> </div> ) : ( competitions != undefined && competitions.length > 0 && ( <Box> <CompetitionList competitions={competitions} /> <Box sx={{ display: "flex", justifyContent: "center", mt: 5, }} > <Link href={`/categories?id=${selectedCategory}`} passHref> <ButtonCustom text="Explore All" onClick={() => exploreAll(selectedCategory)} /> </Link> </Box> </Box> ) )} </> )} <Box sx={{ mt: 10 }} /> <BackgroundPhotoFooter /> <Footer /> </> ); }
错误原因分析
- 目录拼写错误:原目录
/cateogies/page.tsx拼写错误,Next.js无法识别该路径,导致/categories页面预渲染失败。 - 客户端组件数据访问异常:首页标记为
"use client",预渲染阶段useEffect不会执行,categories初始为空数组,后续尝试访问data[0].id会抛出Cannot read properties of undefined (reading 'id')错误。 - Keycloak上下文预渲染冲突:预渲染阶段无法获取客户端的Keycloak认证状态,
authLoading在构建时的不稳定状态会导致组件渲染逻辑异常。 - 缺失/competition页面:错误信息中提到的
/competition/page不存在,Next.js预渲染时无法解析该路径。
修复方法
1. 修正目录拼写
将src/app/cateogies重命名为src/app/categories,确保与代码中跳转路径(如<Link href="/categories?id=${selectedCategory}">)一致。
2. 修复数据获取逻辑
在getCategories的回调中增加空数组判断,避免访问undefined项:
getCategories() .then((data) => { setCategories(data); if (data.length > 0) { setSelectedCategory(data[0].id); } }) .catch((err) => console.error(err));
若数据可在服务端获取,建议去掉"use client"标记,将页面改为服务端组件,直接用async/await获取数据:
// 去掉"use client" export default async function Home() { const categories = await getCategories(); const competitions = await getCompetitions(); // 后续逻辑... }
3. 处理Keycloak预渲染问题
预渲染阶段无法获取客户端认证状态,可修改authLoading的兜底逻辑,或使用dynamic导入禁用预渲染:
import dynamic from 'next/dynamic'; const Home = dynamic(() => import('./page'), { ssr: false }); export default Home;
4. 处理缺失的/competition页面
- 若该页面为必要页面,创建
src/app/competition/page.tsx; - 若为代码误写,检查所有
<Link>及路由跳转逻辑,修正错误路径。
Next.js 14 页面预渲染正确配置步骤
- 区分页面类型:
- 服务端组件(默认):无需标记,可直接用
async/await获取数据,构建时预渲染静态内容; - 客户端组件:添加
"use client"标记,适合交互逻辑,预渲染仅生成静态骨架,数据在客户端加载。
- 服务端组件(默认):无需标记,可直接用
- 静态生成(SSG):
- 服务端组件中直接获取数据,Next.js构建时自动生成静态HTML;
- 动态路由需使用
generateStaticParams生成参数:export async function generateStaticParams() { const categories = await getCategories(); return categories.map((cat) => ({ id: cat.id.toString() })); }
- 增量静态再生(ISR):
- 在页面组件中添加
revalidate属性,指定页面重新生成的时间间隔:export const revalidate = 60; // 每60秒重新生成页面
- 在页面组件中添加
- 禁用预渲染场景:
- 依赖客户端状态(如认证、本地存储)的页面,使用
dynamic导入并设置ssr: false; - 或在
useEffect中执行仅客户端逻辑:useEffect(() => { // 仅客户端执行的逻辑 }, []);
- 依赖客户端状态(如认证、本地存储)的页面,使用
内容的提问来源于stack exchange,提问作者riki
相关产品推荐
相关产品推荐

