feat: 接入预览模式(Next.js Draft Mode)

This commit is contained in:
2026-08-07 15:34:59 +08:00
parent 1abadd04b8
commit 2457d5ec69
9 changed files with 203 additions and 6 deletions
+81
View File
@@ -0,0 +1,81 @@
import { draftMode, cookies } from "next/headers";
import { fetchAPI } from "./api-client";
import type { WagtailPageSummary } from "@/types/wagtail";
/**
* Wagtail 预览模式(wagtail_headless_preview + Next.js Draft Mode)。
* 详见 documents/设计方案分析与完善版.md §2.16,后端实现见 apps/api/urls.py
* 的 PagePreviewAPIViewSet(挂载在 /api/v2/page_preview/)。
*/
export const PREVIEW_CONTENT_TYPE_COOKIE = "wagtail_preview_content_type";
export const PREVIEW_TOKEN_COOKIE = "wagtail_preview_token";
/** content_type`app_label.model`,小写)→ 对应前端路由的解析函数 */
const PREVIEW_PATH_RESOLVERS: Record<
string,
(page: WagtailPageSummary) => string
> = {
"home.homepage": () => "/",
"blog.blogpage": (page) => `/blog/${page.meta.slug}`,
"products.productpage": (page) => `/products/${page.meta.slug}`,
"cases.casestudypage": (page) => `/cases/${page.meta.slug}`,
"solutions.solutionpage": (page) => `/solutions/${page.meta.slug}`,
};
/** 根据 content_type 解析预览内容应跳转到的前端路径,未知类型返回 null。 */
export function resolvePreviewPath(
contentType: string,
page: WagtailPageSummary,
): string | null {
const resolver = PREVIEW_PATH_RESOLVERS[contentType];
return resolver ? resolver(page) : null;
}
/**
* 拉取草稿页面数据。`fields` 省略时仅返回 Wagtail API 默认字段(含 meta.slug/meta.type),
* 用于 /preview 路由校验 token 是否有效。
*/
export async function fetchPreviewPage<T extends WagtailPageSummary>(
contentType: string,
token: string,
fields?: string,
): Promise<T | null> {
const query = new URLSearchParams({ content_type: contentType, token });
if (fields) {
query.set("fields", fields);
}
try {
return await fetchAPI<T>(`/api/v2/page_preview/?${query.toString()}`, {
revalidate: 0,
});
} catch {
return null;
}
}
/**
* 在页面组件中调用:若 Draft Mode 已开启且 cookie 中记录的 content_type
* 与当前页面类型一致,返回草稿数据;否则回退到 `fallback()`(正常已发布内容)。
*/
export async function getPreviewOrFallback<T extends WagtailPageSummary>(
expectedContentType: string,
fields: string,
fallback: () => Promise<T | null>,
): Promise<T | null> {
const { isEnabled } = await draftMode();
if (!isEnabled) {
return fallback();
}
const cookieStore = await cookies();
const contentType = cookieStore.get(PREVIEW_CONTENT_TYPE_COOKIE)?.value;
const token = cookieStore.get(PREVIEW_TOKEN_COOKIE)?.value;
if (contentType !== expectedContentType || !token) {
return fallback();
}
const preview = await fetchPreviewPage<T>(expectedContentType, token, fields);
return preview ?? fallback();
}