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

如何解决Contentful与Next.js App Router集成时无法获取内容的问题

问题:Next.js App Router集成Contentful无法获取内容,返回400错误

错误详情

错误触发于src\app\utils\getContent.jsx第9行调用client.getEntries时,具体返回信息:

Error: {
"status": 400,
"statusText": "Bad Request",
"message": "The query you sent was invalid. Probably a filter or ordering specification is not applicable to the type of a field.",
"details": {
"errors": [
{
"name": "unknownContentType",
"value": "DOESNOTEXIST"
}
]
}
}

相关代码

1. resources/[slug]/Page.jsx

import Button from "../components/Button/Button";
import { createClient } from "contentful";
import Image from "next/image";
import Link from "next/link";
import { GoArrowLeft } from "react-icons/go";

const client = createClient({
  space: process.env.CONTENTFUL_SPACE_ID,
  accessToken: process.env.CONTENTFUL_ACCESS_KEY,
});

export async function generateMetaData({ params }) {
  try {
    const resource = await fetchResource(params);
    if (!resource)
      return {
        title: "404 Not Found",
        description:
          "The resource you are looking for does not exist or it has been removed.",
      };
    return {
      title: resource.fields.title,
      description: resource.fields.description,
    };
  } catch (error) {
    console.error(error);
    return {
      title: "404 Not Found",
      description:
        "The resource you are looking for does not exist or it has been removed.",
    };
  }
}

export async function generateStaticParams() {
  const res = await client.getEntries({
    content_type: "resource",
  });

  return res.items.map((item) => ({
    slug: item.fields.slug,
  }));
}

async function fetchResource({ slug }) {
  const res = await client.getEntries(
    {
      content_type: "resource",
      "fields.slug": slug,
    },
    {
      revalidate: 345600, // Add revalidation here
    }
  );

  return res.items[0];
}

export default async function ResourceDetails({ params }) {
  const resource = await fetchResource(params);

  return (
    <section className="mt-8">
      <Link scroll={false} href="/" className="flex items-center gap-x-1 pl-2">
        <GoArrowLeft color="#F7F7F7" size={24} />
        <span className=" font-semibold text-text text-base">Back</span>
      </Link>
      <div className="flex flex-col md:flex-row mt-5 gap-x-10 lg:gap-x-16 justify-center items-center">
        <div className=" bg-super-dark-gray flex relative items-center justify-center md:pb-[25%] md:pt-[25%] pt-[50%] pb-[50%] rounded-2xl w-full md:w-1/2 border-4 border-outline border-opacity-15">
          <div className="absolute w-2/3 rounded-3xl overflow-hidden  shadow-shine bg-transparent bg-opacity-0 ">
            <Image
              priority={true}
              alt={resource.fields.title}
              src={"https:" + resource.fields.thumbnail.fields.file.url}
              className="h-full w-full"
              width={800}
              height={800}
            />
          </div>
        </div>
        <div className="w-full mt-12 md:mt-0 md:w-1/2">
          <div className="flex flex-col gap-y-3 items-start">
            <h1 className="text-h4 xl:text-h3 font-bold">
              {resource.fields.title}
            </h1>
            <p className=" text-text text-base xl:text-h6 2xl:text-h5 max-w-[50ch] text-balance pb-3 ">
              {resource.fields.description}
            </p>
            <Button
              target="_blank"
              rel="noopener noreferrer"
              href={resource.fields.link}
            >
              View Source
            </Button>
          </div>

          <div className="flex flex-col gap-x-2 mt-12 divide-y-2 divide-outline divide-opacity-20 text-xs xl:text-base 2xl:text-h6">
            <div className=" gap-x-1 grid grid-cols-12 border-t-2 border-outline border-opacity-20 py-2">
              <h2 className=" font-semibold col-span-4">Category</h2>
              <span className=" col-span-8 text-text px-1">
                {resource.fields.category.fields.category}
              </span>
            </div>
            <div className=" gap-x-1 grid grid-cols-12 ">
              <h2 className=" font-semibold col-span-4 pt-2">Tags</h2>
              <span className="flex flex-col col-span-8  text-text">
                {resource.fields.tags.map((tag) => (
                  <span
                    className=" py-2 border-b-2 border-outline border-opacity-20 px-1"
                    key={tag.sys.id}
                  >
                    {tag.fields.tag}
                  </span>
                ))}
              </span>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

2. src/app/utils/getContent.jsx

import { createClient } from "contentful";

export async function getContent({ contentType, ...rest }) {
  const client = createClient({
    space: process.env.CONTENTFUL_SPACE_ID,
    accessToken: process.env.CONTENTFUL_ACCESS_TOKEN,
  });

  const { items, total } = await client.getEntries({
    content_type: contentType,
    ...rest,
  });

  return { items, total };
}

问题诊断

错误明确显示请求传入的content_type值为DOESNOTEXIST,但你在Page.jsx中调用的是content_type: "resource",说明项目中存在其他调用getContent的地方,传入了无效的contentType参数,而非你预期的合法内容类型ID。

解决方案

  1. 排查getContent的所有调用:全局搜索项目中使用getContent的代码,检查每个调用传入的contentType值是否为Contentful后台已存在的内容类型ID(比如你的resource类型),替换掉DOESNOTEXIST这类无效值。
  2. 确认内容类型ID正确性:登录Contentful后台,进入对应内容类型的设置页面,核实其ID确实为resource(注意ID是系统生成的唯一标识,不是显示名称)。
  3. 统一环境变量名:注意到Page.jsx中使用的是CONTENTFUL_ACCESS_KEY,而getContent.jsx中用的是CONTENTFUL_ACCESS_TOKEN,需确保这两个环境变量指向同一个有效令牌,或统一为同一个变量名避免混淆。
  4. 添加参数校验:在getContent函数中增加参数校验,提前拦截无效输入:
export async function getContent({ contentType, ...rest }) {
  if (!contentType || typeof contentType !== 'string') {
    throw new Error('必须传入有效的contentType参数');
  }
  // 原有客户端创建及请求逻辑
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 10:17:03