Next.js 连接 Strapi 的推荐边界是:页面和布局保持 Server Component,在服务端数据层调用 Strapi;只有交互需要时才下沉 Client Component。私密 API Token 只存在服务器环境变量中,不能使用 NEXT_PUBLIC_ 前缀。
目录
环境变量
STRAPI_URL=http://localhost:1337
STRAPI_API_TOKEN=replace-with-server-only-token
公开内容可以配置 Strapi Public 角色的只读权限,不一定需要 Token。草稿、会员内容和后台预览必须在服务端鉴权。
建立统一请求函数
// src/lib/strapi.ts
const STRAPI_URL = process.env.STRAPI_URL
if (!STRAPI_URL) {
throw new Error("Missing STRAPI_URL")
}
type StrapiResponse<T> = {
data: T
meta?: Record<string, unknown>
}
export async function strapiFetch<T>(
path: string,
options: RequestInit & { next?: { revalidate?: number; tags?: string[] } } = {},
): Promise<StrapiResponse<T>> {
const token = process.env.STRAPI_API_TOKEN
const response = await fetch(`${STRAPI_URL}${path}`, {
...options,
headers: {
Accept: "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.headers,
},
next: options.next ?? { revalidate: 300 },
})
if (!response.ok) {
throw new Error(`Strapi ${response.status}: ${await response.text()}`)
}
return response.json()
}
统一封装能确保错误不会被静默吞掉,并让认证、缓存和日志策略集中维护。
在 Server Component 中读取列表
type Article = {
documentId: string
title: string
slug: string
excerpt: string
}
export default async function BlogPage() {
const { data: articles } = await strapiFetch<Article[]>(
"/api/articles?fields[0]=title&fields[1]=slug&fields[2]=excerpt",
{ next: { revalidate: 300, tags: ["articles"] } },
)
return (
<ul>
{articles.map((article) => (
<li key={article.documentId}>{article.title}</li>
))}
</ul>
)
}
Strapi 5 文档通常使用 documentId,内容字段采用扁平结构。如果代码依赖 Strapi 4 的 attributes,需要先确认版本并迁移类型。
查询详情与关联内容
复杂查询建议用 qs 构造,避免手写长方括号 URL:
import qs from "qs"
const query = qs.stringify(
{
filters: { slug: { $eq: slug } },
fields: ["title", "slug", "excerpt", "content"],
populate: {
cover: { fields: ["url", "alternativeText"] },
author: { fields: ["name"] },
},
},
{ encodeValuesOnly: true },
)
const { data } = await strapiFetch<Article[]>(`/api/articles?${query}`)
const article = data[0]
列表页和详情页应该分别定义查询形状,不要为了复用一个函数让列表页也加载完整正文。关联查询详见 Strapi populate 指南。
缓存如何选择
| 内容 | 建议 |
|---|---|
| 公开且很少更新的文章 | revalidate 定时再验证 |
| 登录用户私有数据 | cache: "no-store" |
| 发布后需要立即刷新 | cache tag + webhook 触发再验证 |
| 后台草稿预览 | 动态请求,不与公开缓存混用 |
不要给带用户身份或私密 Token 的个性化响应使用公共缓存。缓存键、请求头和部署平台行为需要在实际环境验证。
用 Route Handler 隔离客户端请求
浏览器交互确实需要请求 Strapi 时,可以让客户端访问自己的 Route Handler:
// app/api/articles/search/route.ts
import { NextRequest, NextResponse } from "next/server"
export async function GET(request: NextRequest) {
const query = request.nextUrl.searchParams.get("q")?.trim() ?? ""
if (query.length < 2) {
return NextResponse.json({ data: [] })
}
const result = await searchArticles(query)
return NextResponse.json(result)
}
这样可以在服务端限制输入、速率和返回字段,同时隐藏 Strapi 地址与私密 Token。但公开 API 地址本身不是秘密,真正的安全仍依赖 Strapi 权限配置。
图片域名与 URL
Strapi 媒体 URL 可能是相对地址:
export function strapiMedia(url?: string) {
if (!url) return undefined
if (url.startsWith("http")) return url
return `${process.env.STRAPI_URL}${url}`
}
使用 Next.js Image 时,还需要在 Next 配置中允许对应的远程图片域名。生产环境最好通过对象存储或 CDN 提供媒体,并确认替代文本真实可用。
常见错误
- 在 Client Component 中读取私密 Token。
- 所有请求都使用
populate=*,导致响应不断膨胀。 - 捕获错误后返回空数组,让线上故障看起来像“没有内容”。
- 把 Strapi 4 与 Strapi 5 的响应类型混在一起。
- 为私有内容错误配置静态缓存。
- 只在前端隐藏按钮,却给 Public 角色开放写权限。
内容模型与权限还未确定时,先阅读 Strapi 5 入门指南。
参考资料
继续阅读
Strapi 5 populate 完整指南:关联、媒体与嵌套查询
解释 Strapi 5 REST API 为什么默认不返回关联数据,并通过 populate=*、字段选择、嵌套 populate 和 qs 构造器展示可维护的查询方式与性能边界。
10 分钟React Hydration Failed:原因、定位与修复
系统排查服务器 HTML 与客户端首次渲染不一致的问题,覆盖时间、随机数、浏览器 API、无效 HTML、客户端存储、第三方扩展和 suppressHydrationWarning 的边界。
9 分钟Next.js Server Actions 与 Fetch 怎么选
比较表单 Server Action、客户端 fetch 和直接调用后端 API 的边界,说明认证、渐进增强、缓存刷新、文件上传与公开 API 场景下的选择方法。
订阅 FreeMac
每周精选:免费 Mac 软件评测、可信来源更新、替代方案和少折腾指南。