feat: 接入预览模式(Next.js Draft Mode)
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { Metadata } from "next";
|
||||
import { getBlogBySlug } from "@/services/blog.service";
|
||||
import { getPreviewOrFallback } from "@/services/preview.service";
|
||||
import { BlockRenderer } from "@/blocks/BlockRenderer";
|
||||
import type { BlogPageDetail } from "@/types/wagtail";
|
||||
|
||||
interface BlogDetailPageProps {
|
||||
params: Promise<{ slug: string }>;
|
||||
@@ -17,7 +19,11 @@ export async function generateMetadata({
|
||||
|
||||
export default async function BlogDetailPage({ params }: BlogDetailPageProps) {
|
||||
const { slug } = await params;
|
||||
const post = await getBlogBySlug(slug).catch(() => null);
|
||||
const post = await getPreviewOrFallback<BlogPageDetail>(
|
||||
"blog.blogpage",
|
||||
"body,intro,published_at",
|
||||
() => getBlogBySlug(slug).catch(() => null),
|
||||
);
|
||||
|
||||
if (!post) {
|
||||
notFound();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { Metadata } from "next";
|
||||
import { getCaseStudyBySlug } from "@/services/case.service";
|
||||
import { getPreviewOrFallback } from "@/services/preview.service";
|
||||
import { BlockRenderer } from "@/blocks/BlockRenderer";
|
||||
import type { CaseStudyPageDetail } from "@/types/wagtail";
|
||||
|
||||
interface CaseStudyDetailPageProps {
|
||||
params: Promise<{ slug: string }>;
|
||||
@@ -19,7 +21,11 @@ export default async function CaseStudyDetailPage({
|
||||
params,
|
||||
}: CaseStudyDetailPageProps) {
|
||||
const { slug } = await params;
|
||||
const item = await getCaseStudyBySlug(slug).catch(() => null);
|
||||
const item = await getPreviewOrFallback<CaseStudyPageDetail>(
|
||||
"cases.casestudypage",
|
||||
"body,client_name,industry,published_at,summary",
|
||||
() => getCaseStudyBySlug(slug).catch(() => null),
|
||||
);
|
||||
|
||||
if (!item) {
|
||||
notFound();
|
||||
|
||||
+15
-1
@@ -1,4 +1,5 @@
|
||||
import type { Metadata } from "next";
|
||||
import { draftMode } from "next/headers";
|
||||
import "./globals.css";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
@@ -16,11 +17,24 @@ export const metadata: Metadata = {
|
||||
description: "企业级 Headless CMS 驱动的现代化官网",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
export default async function RootLayout({ children }: LayoutProps<"/">) {
|
||||
const { isEnabled: isPreview } = await draftMode();
|
||||
|
||||
return (
|
||||
<html lang="zh-hans" className="h-full antialiased">
|
||||
<body className="min-h-full flex flex-col font-sans">
|
||||
<ReactQueryProvider>
|
||||
{isPreview && (
|
||||
<div className="flex items-center justify-center gap-4 bg-amber-400 px-4 py-2 text-sm font-medium text-amber-950">
|
||||
<span>预览模式:当前显示的是未发布的草稿内容</span>
|
||||
<a
|
||||
href="/preview/disable"
|
||||
className="underline underline-offset-2"
|
||||
>
|
||||
退出预览
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<Header />
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
|
||||
+7
-1
@@ -1,13 +1,19 @@
|
||||
import type { Metadata } from "next";
|
||||
import { getHomePage } from "@/services/page.service";
|
||||
import { getPreviewOrFallback } from "@/services/preview.service";
|
||||
import { BlockRenderer } from "@/blocks/BlockRenderer";
|
||||
import type { HomePageDetail } from "@/types/wagtail";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "首页",
|
||||
};
|
||||
|
||||
export default async function Home() {
|
||||
const homePage = await getHomePage().catch(() => null);
|
||||
const homePage = await getPreviewOrFallback<HomePageDetail>(
|
||||
"home.homepage",
|
||||
"body,seo_title_override,seo_description_override",
|
||||
() => getHomePage().catch(() => null),
|
||||
);
|
||||
|
||||
if (!homePage) {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { draftMode, cookies } from "next/headers";
|
||||
import {
|
||||
PREVIEW_CONTENT_TYPE_COOKIE,
|
||||
PREVIEW_TOKEN_COOKIE,
|
||||
} from "@/services/preview.service";
|
||||
|
||||
/** 退出预览模式:关闭 Draft Mode 并清除预览专用 cookie,重定向回首页。 */
|
||||
export async function GET(request: NextRequest) {
|
||||
const draft = await draftMode();
|
||||
draft.disable();
|
||||
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.delete(PREVIEW_CONTENT_TYPE_COOKIE);
|
||||
cookieStore.delete(PREVIEW_TOKEN_COOKIE);
|
||||
|
||||
return NextResponse.redirect(new URL("/", request.url));
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { draftMode, cookies } from "next/headers";
|
||||
import {
|
||||
fetchPreviewPage,
|
||||
resolvePreviewPath,
|
||||
PREVIEW_CONTENT_TYPE_COOKIE,
|
||||
PREVIEW_TOKEN_COOKIE,
|
||||
} from "@/services/preview.service";
|
||||
import type { WagtailPageSummary } from "@/types/wagtail";
|
||||
|
||||
/**
|
||||
* Wagtail 编辑器点击"预览"后会重定向到此路由(详见 wagtailcms/settings/base.py
|
||||
* 中 WAGTAIL_HEADLESS_PREVIEW.CLIENT_URLS)。校验 token 有效后开启 Next.js
|
||||
* Draft Mode 并重定向到对应内容的前端路由,由页面组件通过 getPreviewOrFallback
|
||||
* 拉取草稿数据渲染。
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
const contentType = request.nextUrl.searchParams.get("content_type");
|
||||
const token = request.nextUrl.searchParams.get("token");
|
||||
|
||||
if (!contentType || !token) {
|
||||
return NextResponse.json(
|
||||
{ error: "缺少 content_type 或 token 参数" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
// 先校验 token 是否有效并取回 slug,未开启 Draft Mode,避免暴露无效预览。
|
||||
const page = await fetchPreviewPage<WagtailPageSummary>(contentType, token);
|
||||
const path = page ? resolvePreviewPath(contentType, page) : null;
|
||||
|
||||
if (!page || !path) {
|
||||
return NextResponse.json(
|
||||
{ error: "预览链接无效或已过期" },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
const draft = await draftMode();
|
||||
draft.enable();
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
const cookieStore = await cookies();
|
||||
const cookieOptions = {
|
||||
httpOnly: true,
|
||||
sameSite: "lax" as const,
|
||||
secure: isProduction,
|
||||
path: "/",
|
||||
};
|
||||
cookieStore.set(PREVIEW_CONTENT_TYPE_COOKIE, contentType, cookieOptions);
|
||||
cookieStore.set(PREVIEW_TOKEN_COOKIE, token, cookieOptions);
|
||||
|
||||
return NextResponse.redirect(new URL(path, request.url));
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { Metadata } from "next";
|
||||
import { getProductBySlug } from "@/services/product.service";
|
||||
import { getPreviewOrFallback } from "@/services/preview.service";
|
||||
import { BlockRenderer } from "@/blocks/BlockRenderer";
|
||||
import type { ProductPageDetail } from "@/types/wagtail";
|
||||
|
||||
interface ProductDetailPageProps {
|
||||
params: Promise<{ slug: string }>;
|
||||
@@ -19,7 +21,11 @@ export default async function ProductDetailPage({
|
||||
params,
|
||||
}: ProductDetailPageProps) {
|
||||
const { slug } = await params;
|
||||
const product = await getProductBySlug(slug).catch(() => null);
|
||||
const product = await getPreviewOrFallback<ProductPageDetail>(
|
||||
"products.productpage",
|
||||
"body,summary",
|
||||
() => getProductBySlug(slug).catch(() => null),
|
||||
);
|
||||
|
||||
if (!product) {
|
||||
notFound();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import type { Metadata } from "next";
|
||||
import { getSolutionBySlug } from "@/services/solution.service";
|
||||
import { getPreviewOrFallback } from "@/services/preview.service";
|
||||
import { BlockRenderer } from "@/blocks/BlockRenderer";
|
||||
import type { SolutionPageDetail } from "@/types/wagtail";
|
||||
|
||||
interface SolutionDetailPageProps {
|
||||
params: Promise<{ slug: string }>;
|
||||
@@ -19,7 +21,11 @@ export default async function SolutionDetailPage({
|
||||
params,
|
||||
}: SolutionDetailPageProps) {
|
||||
const { slug } = await params;
|
||||
const solution = await getSolutionBySlug(slug).catch(() => null);
|
||||
const solution = await getPreviewOrFallback<SolutionPageDetail>(
|
||||
"solutions.solutionpage",
|
||||
"body,industry,summary",
|
||||
() => getSolutionBySlug(slug).catch(() => null),
|
||||
);
|
||||
|
||||
if (!solution) {
|
||||
notFound();
|
||||
|
||||
Reference in New Issue
Block a user