Puck AI + Next.js 实战:用 AI 副驾驶为任意路由生成并发布可视化页面
【免费下载链接】puckThe visual editor for React.项目地址: https://gitcode.com/GitHub_Trending/puc/puck
Puck 是开源的 React 可视化编辑器,它允许你用自己的 React 组件快速搭建页面构建器;Puck AI 则在同样的组件体系之上,让你通过对话让 AI 组装现有组件、或按需现场生成新组件来产出页面。本指南基于仓库中的recipes/next-ai示例(README),完整讲解如何在 Next.js App Router 项目中接入 Puck 编辑器与 Puck AI 云客户端,实现"任意路径 + /edit 即进入编辑器、对话生成页面、一键发布、访问即渲染"的完整闭环。读完本文,你将掌握 config / 编辑器 / 渲染器三要素的协作方式、AI 插件与 Cloud Client 的分工、Assembly / Design 两种 AI 模式的区别,以及整套路由重写与数据持久化的实现原理。
核心概念:Puck 的三要素与 Puck AI 的两部分
Puck 的三个组成部分
Puck 可视化编辑器由三个主要部分构成:config(配置)、编辑器(editor)与渲染器(renderer)。
Config:注册组件与字段
config用于注册用户可以在编辑器中使用的组件,以及他们可以编辑的字段。以本 recipe 中的最小配置为例:
const config = { components: { HeadingBlock: { fields: { title: { type: "text" }, }, render: ({ title }) => <h1>{title}</h1>, }, }, };真实仓库中的 puck.config.tsx 在此基础上增加了defaultProps并补充了外层容器样式,使其开箱即可编辑:
import type { Config } from "@puckeditor/core"; type Props = { HeadingBlock: { title: string }; }; export const config: Config<Props> = { components: { HeadingBlock: { fields: { title: { type: "text" }, }, defaultProps: { title: "Heading", }, render: ({ title }) => ( <div style={{ padding: 64 }}> <h1>{title}</h1> </div> ), }, }, };从源码可以看出两个值得注意的细节:Config<Props>提供了泛型类型约束,让fields、defaultProps与render之间共享类型安全;defaultProps让组件在刚拖入画布时就有合理的初始值,避免空白组件。这也是Assembly mode(组装模式)拼装页面时的"零件清单"——AI 只能从这套配置中挑选组件。
编辑器:<Puck>组件
<Puck>组件渲染编辑器本体。它接收 config,将页面导出为 JSON(即 Puck 的 Data 结构),并接受data作为初始页面数据以编辑已有页面:
<Puck config={config} // 编辑器可用的组件 data={data} // 待编辑的页面 JSON onPublish={(data) => { // 将数据保存到你的数据库 }} />本 recipe 中编辑器的实际装配位于 app/puck/[...puckPath]/client.tsx,后续会展开讲解它与 AI 插件的集成。
渲染器:<Render>组件
<Render>负责渲染页面,它需要页面 JSON 以及创建该页面时所用的 config:
<Render config={config} // 创建页面时使用的组件 data={data} // 待渲染的页面 JSON />值得强调的是:渲染用的 config 必须与编辑时一致,否则页面无法正确还原。在 AI Design mode 下,config 还可能是动态生成的——这正是本 recipe 中withDynamicConfig存在的意义(见下文 AI 集成部分)。
Puck AI 的两个组成部分
本 recipe 将 Puck AI 作为"副驾驶(copilot)"引入,它由两部分组成:AI 插件(浏览器端)与Cloud Client(服务端)。
AI 插件:编辑器内的聊天界面
AI 插件在编辑器中渲染聊天面板,并把每条消息发送给你服务器上的 Cloud Client:
const aiPlugin = createAiPlugin(); function Editor() { return <Puck plugins={[aiPlugin]} config={config} data={data} />; }本 recipe 对插件做了更细致的配置(app/puck/[...puckPath]/client.tsx):
"use client"; import type { Data } from "@puckeditor/core"; import { Puck, blocksPlugin, outlinePlugin } from "@puckeditor/core"; import { createAiPlugin, withDynamicConfig } from "@puckeditor/plugin-ai"; import config from "../../../puck.config"; const aiPlugin = createAiPlugin({ // 允许用户在 design 与 assembly 模式间切换 designMode: { visible: true, }, // 默认选中 design 模式 defaultMode: "design", }); // 将 AI 插件置于侧边栏第一个位置 const plugins = [aiPlugin, blocksPlugin(), outlinePlugin()]; export function Client({ path, data }: { path: string; data: Data }) { const configWithDesignedComponents = withDynamicConfig(config, data); return ( <Puck plugins={plugins} data={data} config={configWithDesignedComponents} onPublish={async (data) => { await fetch("/api/pages", { method: "post", body: JSON.stringify({ data, path }), }); }} /> ); }这段代码里有两个关键点:
createAiPlugin({ designMode: { visible: true }, defaultMode: "design" }):让用户在编辑器中可以切换 Design / Assembly 模式,并默认使用 Design 模式。withDynamicConfig(config, data):当 AI 在 Design 模式下生成了新组件后,这些组件会被合并进运行时 config。withDynamicConfig正是用来把"设计出来的组件"动态合并进原有 config 的辅助函数,确保<Puck>与后续的<Render>都能识别这些新组件。
此外onPublish通过 POST 把{ data, path }发送到/api/pages接口完成保存。
Cloud Client:连接 Puck 云的服务端 API
Cloud Client 提供将你的服务器连接到 Puck 云的 API。本 recipe 使用了它的puckHandler同类机制(实际实现在@puckeditor/cloud-client包中)——它接收每条聊天消息、转发给 Puck 云,并把响应流式返回给浏览器中的插件:
const handleRequest = (request: NextRequest) => { return puckHandler(request, { ai: { context: "We are Google. You create Google landing pages.", }, }); }; export const DELETE = handleRequest; export const GET = handleRequest; export const POST = handleRequest;本 recipe 的完整实现位于 app/api/puck/[...all]/route.ts:
import type { NextRequest } from "next/server"; import { puckHandler } from "@puckeditor/cloud-client"; const handleRequest = (request: NextRequest): Promise<Response> => { return puckHandler(request, { ai: { // 替换为你的业务上下文 context: "We are Google. You create Google landing pages.", designMode: { // 允许 AI 使用 "design mode" 生成新组件 allowed: true, // 约束组件生成规则,替换为你自己的指令 instructions: ` #### Color Palette Always use the following colors: * Primary: \`#1976d2\` * Secondary: \`#9c27b0\` `, }, }, }); }; export const DELETE = handleRequest; export const GET = handleRequest; export const POST = handleRequest;这里的[...all]动态路由段保证所有来自 AI 插件的请求(无论方法或路径后缀)都会落到同一个处理器上。context是注入 AI 的全局业务语境,designMode.instructions则用来约束 AI 生成组件时的风格(如示例中的品牌色板)。
Puck AI 的两种工作模式
Puck AI 可以通过两种方式构建页面:
- Assembly mode(组装模式):只用你 config 中已有的组件来拼装页面,行为可预测、样式可控;
- Design mode(设计模式):当现有组件不足以表达意图时,AI 可以按需生成全新组件。
本 recipe 开箱即用地启用了Design mode——通过插件侧的defaultMode: "design"、designMode.visible: true以及 Cloud Client 侧的designMode.allowed: true三处配置共同完成。
运行 recipe:从 API Key 到发布页面
1. 添加 Puck API Key
先创建账户并生成 API Key,然后在.env.local文件中写入:
PUCK_API_KEY=your-api-keyAI 插件与 Cloud Client 之间的鉴权依赖该密钥,请勿提交到版本库。
2. 启动开发服务器
在recipes/next-ai目录下运行:
npm run dev根据 package.json,该命令实际执行next dev --turbo,依赖next ^16.2.11与react ^19.2.1,并包含@puckeditor/core、@puckeditor/plugin-ai、@puckeditor/cloud-client三个关键包。
服务器启动后:
- 访问 http://localhost:3000 查看首页;
- 访问 http://localhost:3000/edit 用 Puck 编辑它。
3. 用 Puck AI 创建页面
导航到 http://localhost:3000/edit,点击左侧边栏的AI按钮,输入提示词(prompt)并回车,AI 即开始组装或生成组件。
4. 发布页面
页面完成后,点击顶部工具栏的Publish保存结果,然后访问 http://localhost:3000 查看已发布的页面。
更强大的是:你可以通过访问/your/path/edit在任意路径下创建页面并发布,发布后/your/path路由就会渲染该页面。
工作原理:/edit 重写、发布与静态渲染的完整链路
路由重写:URL 以 /edit 结尾时进入编辑器
当一个 URL 以/edit结尾时,proxy.ts 会将该请求重写到 Puck 编辑器路由(app/puck/[...puckPath]/page.tsx)。看它的完整实现:
import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; export async function proxy(req: NextRequest) { const res = NextResponse.next({ request: req }); if (req.method === "GET") { // 将匹配 "/[...puckPath]/edit" 的路由重写到 "/puck/[...puckPath]" if (req.nextUrl.pathname.endsWith("/edit")) { const pathWithoutEdit = req.nextUrl.pathname.slice( 0, req.nextUrl.pathname.length - 5 ); const pathWithEditPrefix = `/puck${pathWithoutEdit}`; return NextResponse.rewrite(new URL(pathWithEditPrefix, req.url)); } // 禁用 "/puck/[...puckPath]" 直接访问 if (req.nextUrl.pathname.startsWith("/puck")) { return NextResponse.redirect(new URL("/", req.url)); } } return res; }注意两点设计取舍:
- 把
/foo/edit重写(rewrite)为/puck/foo,对用户透明; - 同时把任何直接访问
/puck开头的请求重定向回首页,避免用户绕过 /edit 约定直接命中内部路由。
编辑器路由 app/puck/[...puckPath]/page.tsx 会加载已保存的页面数据;如果该路径是全新的,则使用内置的空数据初始化:
const EMPTY_PAGE_DATA: Data = { content: [], root: { props: { title: "", }, }, }; // ... const data = getPage(path); return <Client path={path} data={data || EMPTY_PAGE_DATA} />; export const dynamic = "force-dynamic";这段代码还暴露了另一个关键设计:编辑器路由强制force-dynamic(始终保持动态渲染),而前台页面路由则是force-static(静态渲染),两者通过 proxy 隔离,互不影响。
发布:写入数据库并清理 Next.js 缓存
点击Publish时,页面数据被发送到/api/pages接口(app/api/pages/route.ts)。该处理器把 JSON 写入database.json,并清理该页面的 Next.js 缓存:
import { revalidatePath } from "next/cache"; import { NextResponse } from "next/server"; import fs from "fs"; export async function POST(request: Request) { const payload = await request.json(); const existingData = JSON.parse( fs.existsSync("database.json") ? fs.readFileSync("database.json", "utf-8") : "{}" ); const updatedData = { ...existingData, [payload.path]: payload.data, }; fs.writeFileSync("database.json", JSON.stringify(updatedData)); // 清除 Next.js 缓存 revalidatePath(payload.path); return NextResponse.json({ status: "ok" }); }随后,catch-all 路由 app/[...puckPath]/page.tsx 读取同一份数据,并用<Render>渲染它:
const path = `/${puckPath.join("/")}`; const data = getPage(path); if (!data) { return notFound(); } return <Client data={data} />; // 强制 Next.js 生成静态页面,若需要 headers/cookies 等请求期数据请删除此行 export const dynamic = "force-static";前台渲染客户端 app/[...puckPath]/client.tsx 同样使用withDynamicConfig合并 AI 生成的组件后交给<Render>:
"use client"; import type { Data } from "@puckeditor/core"; import { Render } from "@puckeditor/core"; import { withDynamicConfig } from "@puckeditor/plugin-ai"; import config from "../../puck.config"; export function Client({ data }: { data: Data }) { const configWithDesignedComponents = withDynamicConfig(config, data); return <Render config={configWithDesignedComponents} data={data} />; }数据存取:getPage 与 database.json
数据读取统一由 lib/get-page.ts 完成——它从database.json中按路径取出页面数据:
import { Data } from "@puckeditor/core"; import fs from "fs"; export const getPage = (path: string) => { const allData: Record<string, Data> | null = fs.existsSync("database.json") ? JSON.parse(fs.readFileSync("database.json", "utf-8")) : null; return allData ? allData[path] : null; };仓库中现成的 database.json 已经保存了一条示例数据——根路径/下有一个标题为 "Edit this page by adding /edit to the end of the URL" 的HeadingBlock,这就是你首次访问首页时看到的内容。
文件职责总览
下表(来自原 README)列出了实现上述流程的全部文件及其职责:
| 文件 | 用途 |
|---|---|
puck.config.tsx | 定义 Puck 与 Assembly 模式可用的组件、字段与默认 props。在此添加你自己的组件。 |
app/puck/[...puckPath]/page.tsx | 为编辑器加载页面数据。 |
app/puck/[...puckPath]/client.tsx | 渲染带 AI 副驾驶的编辑器,把提示词发送到服务器并发布更改。 |
app/[...puckPath]/page.tsx | 加载并渲染已发布的页面。 |
app/api/pages/route.ts | 保存已发布的页面。 |
app/api/puck/[...all]/route.ts | 处理来自 AI 插件的请求并配置 AI 生成。 |
proxy.ts | 把以/edit结尾的 URL 路由到/puck/[...puckPath]/page.tsx。 |
lib/get-page.ts | 从database.json读取页面数据。可用你自己的数据获取逻辑替换。 |
database.json | 充当本地数据库。可用你自己的数据库方案替换。 |
部署到生产环境前必须完成的五件事
在把本 recipe 部署上线之前,务必完成以下检查:
- 保护编辑器与 API。
/edit、/api/pages与/api/puck路由默认是公开的。必须添加身份认证(authentication)、授权(authorization)与速率限制(rate limits),以保护页面数据与 AI 调用额度。编辑器的页面级代码注释也明确提示:"NB this route is public, and you will need to add authentication"。 - 补齐你的组件库。把
puck.config.tsx中示例用的HeadingBlock替换为你用户真正需要的组件与字段。它决定了用户在编辑器(以及 Assembly 模式)里能拼装的全部能力。 - 设置你的业务上下文。把
app/api/puck/[...all]/route.ts中的示例 Google 语境替换为关于你的产品、受众与内容规则的清晰描述,并利用designMode.instructions约束 AI 生成组件的风格。 - 使用真正的数据库。在
lib/get-page.ts与app/api/pages/route.ts中替换database.json。本地文件在多个服务器实例或 serverless 部署环境下并不可靠(无共享磁盘、实例可能随时回收)。 - 选择合适的渲染策略。
app/[...puckPath]/page.tsx使用了force-static。如果某个页面需要请求期数据(如 headers、cookies 或用户会话),请移除该声明,改回动态渲染。
小结
recipes/next-ai展示了一条完整的"AI 驱动的可视化建站"流水线:proxy.ts用/edit约定把任意路径映射到 Puck 编辑器;createAiPlugin与withDynamicConfig让 AI 既能拼装既有组件、也能动态生成新组件;puckHandler在服务端桥接 Puck 云并注入业务上下文;/api/pages负责持久化并通过revalidatePath让静态页面即时更新。理解这条链路后,你可以在此基础上替换数据层、接入鉴权、扩充组件库,把它演变为真正属于自己业务的可视化页面平台。
【免费下载链接】puckThe visual editor for React.项目地址: https://gitcode.com/GitHub_Trending/puc/puck
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考