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

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 />
    </>
  );
}

错误原因分析

  1. 目录拼写错误:原目录/cateogies/page.tsx拼写错误,Next.js无法识别该路径,导致/categories页面预渲染失败。
  2. 客户端组件数据访问异常:首页标记为"use client",预渲染阶段useEffect不会执行,categories初始为空数组,后续尝试访问data[0].id会抛出Cannot read properties of undefined (reading 'id')错误。
  3. Keycloak上下文预渲染冲突:预渲染阶段无法获取客户端的Keycloak认证状态,authLoading在构建时的不稳定状态会导致组件渲染逻辑异常。
  4. 缺失/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 页面预渲染正确配置步骤

  1. 区分页面类型:
    • 服务端组件(默认):无需标记,可直接用async/await获取数据,构建时预渲染静态内容;
    • 客户端组件:添加"use client"标记,适合交互逻辑,预渲染仅生成静态骨架,数据在客户端加载。
  2. 静态生成(SSG):
    • 服务端组件中直接获取数据,Next.js构建时自动生成静态HTML;
    • 动态路由需使用generateStaticParams生成参数:
      export async function generateStaticParams() {
        const categories = await getCategories();
        return categories.map((cat) => ({ id: cat.id.toString() }));
      }
      
  3. 增量静态再生(ISR):
    • 在页面组件中添加revalidate属性,指定页面重新生成的时间间隔:
      export const revalidate = 60; // 每60秒重新生成页面
      
  4. 禁用预渲染场景:
    • 依赖客户端状态(如认证、本地存储)的页面,使用dynamic导入并设置ssr: false;
    • 或在useEffect中执行仅客户端逻辑:
      useEffect(() => {
        // 仅客户端执行的逻辑
      }, []);
      

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 22:55:57