在 React Router 7 框架模式中集成 Puck 可视化编辑器与 Puck AI:完整实战配方(react-router-ai)
【免费下载链接】puckThe visual editor for React.项目地址: https://gitcode.com/GitHub_Trending/puc/puck
导读
本篇文章围绕 Puck 开源仓库中 recipes/react-router-ai/README.md 这份配方文档展开,讲解如何把可视化页面编辑器Puck与其 AI 副驾驶能力Puck AI集成进React Router 7 框架模式(Framework Mode)应用,实现对任意路由页面的可视化创建、AI 生成与发布。读完本文,你将掌握 Puck 的 Config / Editor / Render 三大核心概念、Puck AI 的 Assembly(组装)与 Design(设计)两种工作模式、/edit路由约定、puckHandler云端代理接入方式,以及生产部署前必须处理的鉴权、数据持久化等关键事项。
Puck 与 Puck AI 是什么
Puck 是面向 React 的开源可视化编辑器。它不是一套预置组件库,而是让你用自己的组件构建页面构建器(page builder)——你只需提供组件与字段配置,Puck 负责拖拽、编辑、布局等全部编辑器能力。
Puck AI在同样的原则之上更进一步:它可以根据自然语言提示,通过组装你已有的组件、或在需要时动态创建全新组件来生成页面。它既可以作为编辑器内的 copilot(AI 副驾驶),也可以通过服务端 API 以无头(headless)方式工作。
这份react-router-ai配方把 Puck 与 Puck AI 接入React Router 的框架模式,使你能够为应用中的任意路由创建和编辑页面。源码位于 recipes/react-router-ai,其中的package.json显示依赖 React Router^7.18.0、React^19.0.0,并通过react-router dev/react-router build运行,属于标准的 React Router 7 框架模式工程。
核心概念:Puck 的三大部分
Puck 可视化编辑器由三部分构成:Config(配置)、Editor(编辑器)与Renderer(渲染器)。理解这三者之间的关系,是使用本配方的前提。
Config:注册组件与字段
Config 注册用户可以在编辑器中用来构建页面的组件,以及他们可以编辑的字段。每个组件通常包含fields(字段定义)、render(渲染函数)和可选的defaultProps(默认属性)。原始配方中的示例配置如下:
const config = { components: { HeadingBlock: { fields: { title: { type: "text" }, }, render: ({ title }) => <h1>{title}</h1>, }, }, };本仓库中对应的真实实现位于 recipes/react-router-ai/puck.config.tsx,它在示例基础上补充了类型定义与默认属性:
import type { Config, Data } from "@puckeditor/core"; export type Props = { HeadingBlock: { title: string }; }; export type UserData = Data<Props>; export const config: Config<Props> = { components: { HeadingBlock: { fields: { title: { type: "text" }, }, defaultProps: { title: "Heading", }, render: ({ title }) => ( <div style={{ padding: 64 }}> <h1>{title}</h1> </div> ), }, }, };从源码结构可以看出:
fields.title声明text类型字段,编辑器会据此自动渲染输入控件;defaultProps.title = "Heading"作为新插入组件时的默认值;render接收组件 props 并返回 React 元素,这是页面渲染的唯一出口;Props与UserData类型把「组件 props」与「页面数据」做了类型化约束,这正是把 Puck 接入 TypeScript 项目时的推荐做法。
Editor:渲染编辑器
<Puck>组件负责渲染编辑器。它接收 config(编辑器可用的组件集合)、data(要编辑的页面 JSON),并通过onPublish回调把页面数据导出。
<Puck config={config} // The components available to the editor data={data} // The page JSON to edit onPublish={(data) => { // Save data to your database }} />配方中的编辑器实现位于 recipes/react-router-ai/app/routes/puck-splat.tsx 的Editor函数内。它使用useFetcher以 POST 方式把页面 JSON 提交给 action 完成保存:
<Puck plugins={plugins} config={configWithDesignedComponents} data={loaderData.data} onPublish={async (data) => { await fetcher.submit( { data: data as UserData }, { action: "", method: "post", encType: "application/json", } ); }} />Renderer:渲染页面
<Render>组件负责渲染已保存的页面。它期望接收页面 JSON 和创建该页面时使用的 config。
<Render config={config} // The components used to create the page data={data} // The page JSON to render />配方将渲染逻辑封装为可复用组件 recipes/react-router-ai/app/components/puck-render.tsx。注意它调用了withDynamicConfig——因为 AI 在 Design 模式下可能动态生成新组件,渲染已生成页面时必须把「设计出来的组件」合并进 config,页面才能正确还原:
import type { Data } from "@puckeditor/core"; import { Render } from "@puckeditor/core"; import { withDynamicConfig } from "@puckeditor/plugin-ai"; import { config } from "../../puck.config"; export function PuckRender({ data }: { data: Data }) { const configWithDesignedComponents = withDynamicConfig(config, data); return <Render config={configWithDesignedComponents} data={data} />; }核心概念:Puck AI 的两大部分
Puck AI 作为 copilot 接入,由两部分组成:AI 插件(浏览器端)与Cloud Client(服务端)。
AI 插件(浏览器)
AI 插件 在编辑器中渲染聊天界面,并把每一条消息发送给你服务器上的 Cloud Client。
const aiPlugin = createAiPlugin(); function Editor() { return <Puck plugins={[aiPlugin]} config={config} data={data} />; }配方中的插件配置(见 puck-splat.tsx)做得更细致——它开放了 Design/Assembly 模式切换,并默认使用 Design 模式,同时把 AI 插件排在侧边栏第一位:
const aiPlugin = createAiPlugin({ // Allow users to switch between design and assembly mode. designMode: { visible: true, }, // Select design mode by default. defaultMode: "design", }); // Place the ai plugin in the first position in the side nav. const plugins = [aiPlugin, blocksPlugin(), outlinePlugin()];这里还同时启用了blocksPlugin()(组件列表插件)与outlinePlugin()(图层树插件),它们共同构成编辑器左侧的导航面板。
Cloud Client(服务器)
Cloud Client 提供把服务器连接到 Puck 云端的 API。本配方使用其中的puckHandlerAPI:它接收每一条聊天消息,转发给 Puck 云端,并把响应流式回传给浏览器中的 AI 插件。
const options = { ai: { context: "We are Google. You create Google landing pages.", }, }; export function loader(args: LoaderFunctionArgs) { return puckHandler(args.request, options); } export function action(args: ActionFunctionArgs) { return puckHandler(args.request, options); }在 React Router 框架模式中,loader处理 GET 请求、action处理 POST 请求——AI 插件的通信既会用到 loader 也会用到 action,因此两者都要委托给puckHandler。配方中的完整实现见 recipes/react-router-ai/app/routes/api.puck.ts,它对 AI 行为做了更完整的配置:
import type { ActionFunctionArgs, LoaderFunctionArgs } from "react-router"; import type { PuckCloudOptions } from "@puckeditor/cloud-client"; import { puckHandler } from "@puckeditor/cloud-client"; const options: PuckCloudOptions = { ai: { // Replace with your business context context: "We are Google. You create Google landing pages.", designMode: { // Allow AI to generate new components using "design mode" allowed: true, // Constrain component generation, replace with your own instructions instructions: ` #### Color Palette Always use the following colors: * Primary: \`#1976d2\` * Secondary: \`#9c27b0\` `, }, }, }; export async function loader(args: LoaderFunctionArgs) { return puckHandler(args.request, options); } export async function action(args: ActionFunctionArgs) { return puckHandler(args.request, options); }其中:
ai.context:业务上下文提示词,决定 AI 生成内容的方向(示例为 Google 落地页);ai.designMode.allowed:是否允许 AI 动态生成新组件;ai.designMode.instructions:对 AI 生成组件的约束指令,示例中约束了品牌色板,你可以替换为自己的设计规范(字体、间距、语气等)。
Puck AI 的两种模式
Puck AI 有两种构建页面的方式:
- Assembly mode(组装模式):仅使用你 config 中已有的组件来构建页面,不会创建新组件;
- Design mode(设计模式):在需要时可以生成全新组件。
本配方默认开启Design mode(allowed: true且defaultMode: "design"),用户在编辑器中还可以通过插件上的designMode.visible: true随时切换两种模式。
运行配方
1. 添加 Puck API Key
首先创建账户并在 Puck 云端 生成 API Key,然后把它写入.env.local文件:
PUCK_API_KEY=your-api-keyPUCK_API_KEY由服务器端的 Cloud Client 读取,用于与 Puck 云端通信;该密钥只应存在于服务端环境,绝不能暴露在浏览器端。
2. 启动开发服务器
npm run dev开发服务器启动后:
- 访问 http://localhost:5173 查看首页;
- 访问 http://localhost:5173/edit 使用 Puck 编辑首页。
3. 用 Puck AI 创建页面
进入 http://localhost:5173/edit,点击左侧边栏的AI按钮,输入提示词并按回车,AI 即会在编辑器中流式生成页面。
4. 发布页面
页面完成后,点击编辑器顶部的Publish保存结果,然后访问 http://localhost:5173 查看已发布的页面。
你还可以通过访问任意路径的/your/path/edit来创建新页面并发布,发布后/your/path路由就会渲染该页面——这正是「为任意路由创建页面」这一配方核心能力的体现。
工作原理:从 /edit 到发布
本配方的核心机制围绕 URL 中的/edit后缀展开:
- 当 URL 以
/edit结尾时,recipes/react-router-ai/app/lib/resolve-puck-path.server.ts 中的resolvePuckPath返回被编辑页面的路径; - recipes/react-router-ai/app/routes/puck-splat.tsx 的 loader 加载已保存的页面,若路径是全新的则返回一个空页面外壳;
- 点击Publish会把页面数据发送到
puck-splat.tsx的 action; - action 把 JSON 写入 recipes/react-router-ai/database.json;
- 该路由随后加载同一份数据,并用
<Render>(经PuckRender封装)渲染发布结果。
resolvePuckPath 的解析逻辑
resolvePuckPath的源码实现值得细看:
export function resolvePuckPath( path = "", // `base` can be any valid origin, it is required for the URL constructor so // we can return a pathname - you can change this if you want, but it isn't // important base = "https://placeholder.com/" ) { const url = new URL(path, base); const segments = url.pathname.split("/"); const isEditorRoute = segments.at(-1) === "edit"; const pathname = isEditorRoute ? segments.slice(0, -1).join("/") : url.pathname; return { isEditorRoute, path: new URL(pathname, base).pathname, }; }它把路径按/切分,判断最后一段是否为edit:
- 若是,则
isEditorRoute = true,去掉最后一段得到被编辑页面的真实路径; - 若否,则按普通页面处理。
例如/docs/intro/edit会解析为「编辑路径/docs/intro」,而/docs/intro会解析为「渲染路径/docs/intro」。base参数只是为了满足URL构造函数的需要而提供的占位 origin,不影响实际路径结果。
loader:加载或初始化页面
puck-splat.tsx的 loader 完整逻辑如下:
export async function loader({ params }: Route.LoaderArgs) { const pathname = params["*"] ?? "/"; const { isEditorRoute, path } = resolvePuckPath(pathname); let page = await getPage(path); // Throw a 404 if we're not rendering the editor and data for the page does not exist if (!isEditorRoute && !page) { throw new Response("Not Found", { status: 404 }); } // Empty shell for new pages if (isEditorRoute && !page) { page = { content: [], root: { props: { title: "", }, }, }; } return { isEditorRoute, path, data: page, }; }要点:
- 非编辑路由且页面数据不存在时,直接抛出 404;
- 编辑路由且页面数据不存在时,返回一个空页面外壳(
content: []表示没有内容块),用户可以从零开始构建; - loader 同时把
isEditorRoute、path、data返回给组件层,由默认导出组件决定渲染编辑器还是发布后的页面。
action:保存页面
export async function action({ params, request }: Route.ActionArgs) { const pathname = params["*"] ?? "/"; const { path } = resolvePuckPath(pathname); const body = (await request.json()) as { data: Data }; await savePage(path, body.data); }action 从请求体中读取{ data }(编辑器onPublish以application/json提交的内容),调用savePage写入数据存储。编辑器端通过useFetcher的fetcher.submit触发这次 POST,且action: ""表示提交给当前路由自身的 action。
数据读写:pages.server.ts
recipes/react-router-ai/app/lib/pages.server.ts 以本地 JSON 文件充当简易数据库:
import path from "path"; import { fileURLToPath } from "url"; import fs from "fs/promises"; import type { Data } from "@puckeditor/core"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const databasePath = path.join(__dirname, "..", "..", "database.json"); export async function getPage(path: string) { const pages = await readDatabase(); return pages[path]; } export async function savePage(path: string, data: Data) { const pages = await readDatabase(); pages[path] = data; await fs.writeFile(databasePath, JSON.stringify(pages), { encoding: "utf8" }); } async function readDatabase() { try { const file = await fs.readFile(databasePath, "utf8"); return JSON.parse(file) as Record<string, Data>; } catch (error: unknown) { console.error(error); return {}; } }从源码可以确认:
- 数据模型是「路径 → 页面 JSON」的映射,如 database.json 中的
"/"键存储首页数据; readDatabase在文件缺失或解析失败时返回空对象(并打印错误),保证首次运行不会崩溃;savePage每次全量写回 JSON 文件——这适合本地演示,但不适合多实例或 serverless 环境(见后文「生产部署前」)。
路由注册
recipes/react-router-ai/app/routes.ts 使用 React Router 7 的声明式路由 API 注册三条路由:
import type { RouteConfig } from "@react-router/dev/routes"; import { route, index } from "@react-router/dev/routes"; export default [ index("routes/_index.tsx"), route("api/puck/*", "routes/api.puck.ts"), route("*", "routes/puck-splat.tsx"), ] satisfies RouteConfig;index("routes/_index.tsx"):首页路由,直接渲染已发布的首页;route("api/puck/*", "routes/api.puck.ts"):Puck AI 的 API 端点,处理来自 AI 插件的请求;route("*", "routes/puck-splat.tsx"):兜底(catch-all)路由,负责所有其他路径的编辑与渲染。
其中首页路由 recipes/react-router-ai/app/routes/_index.tsx 与puck-splat的逻辑同构:调用resolvePuckPath("/")解析首页路径,通过getPage读取页面数据,不存在则 404,然后交给PuckRender渲染。
文件职责速查表
下表汇总了本配方中实现上述流程的关键文件及其职责(见原始文档 README):
| 文件 | 用途 |
|---|---|
puck.config.tsx | 定义可供 Puck 与 Assembly 模式使用的组件、字段与默认属性。需要加入你自己的组件时修改此文件。 |
app/routes.ts | 注册首页、Puck AI API 与兜底页面路由。 |
app/routes/puck-splat.tsx | 加载并保存页面数据,然后渲染编辑器或已发布页面。 |
app/routes/api.puck.ts | 处理来自 AI 插件的请求并配置 AI 生成行为。 |
app/routes/_index.tsx | 加载并渲染首页。 |
app/lib/resolve-puck-path.server.ts | 把/editURL 映射为被编辑页面的路径。 |
app/lib/pages.server.ts | 在database.json中读写页面数据。可替换为你自己的数据获取与保存逻辑。 |
app/components/puck-render.tsx | 使用<Render>渲染已保存的页面数据。 |
database.json | 充当本地数据库。可替换为你自己的数据库方案。 |
生产部署前必须完成的检查清单
在把本配方部署到生产环境之前,请务必确认以下几点(原始文档列出的五项硬性要求):
- 保护编辑器与 API。
/edit路由、发布 action 与/api/puck路由默认都是公开的。必须添加认证(authentication)、授权(authorization)与限流(rate limiting),以保护页面数据与 AI 用量。 - 加入你自己的组件库。将
puck.config.tsx中的示例组件HeadingBlock替换为用户真正需要的组件与字段。这是把「示例配方」变成「业务页面构建器」的关键一步。 - 设置你的业务上下文。将 app/routes/api.puck.ts 中示例的 Google 上下文替换为关于你的产品、目标受众与内容规则的清晰描述。上下文提示词直接决定 AI 生成页面的质量与一致性。
- 使用真正的数据库。替换
database.json与app/lib/pages.server.ts中的函数。本地文件在多服务器实例或 serverless 部署中并不可靠。 - 选择合适的部署策略。本配方使用服务端渲染(SSR)、loader 与 action,因此要部署到兼容 React Router 的服务器运行时。从 react-router.config.ts 可以看到配方显式设置了
ssr: true,且 package.json 提供了react-router build与react-router-serve ./build/server/index.js两个生产构建/运行脚本。
总结
react-router-ai配方展示了一条完整的链路:React Router 7 框架模式的路由与 loader/action承接页面存取,Puck 的 Config/Editor/Render提供可视化编辑能力,Puck AI 的插件与 Cloud Client提供基于自然语言的页面生成能力。你可以沿着这条链路,把任意 React 应用升级为具备 AI 能力的可视化建站系统——唯一需要替换的,就是示例组件与示例数据库。
【免费下载链接】puckThe visual editor for React.项目地址: https://gitcode.com/GitHub_Trending/puc/puck
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考