Payload + Remix 单仓示例:website 子应用的开发、构建与部署指南
本文围绕 Payload 开源仓库中 Remix 示例的 website 子应用 展开,完整讲解该示例在 pnpm monorepo 下的开发启动、生产构建、DIY 部署与 Tailwind CSS 样式配置流程,并结合仓库源码剖析 Vite 构建配置、SSR 入口与 Dockerfile 中的关键实现细节。读完后你能够独立在本地跑通 Payload 管理后台与 Remix 网站双服务,理解 build/server、build/client 产物的部署逻辑,以及 Local API 在 Remix 侧的接入方式。
示例在 monorepo 中的定位
examples/remix 示例的目标是演示如何在 Remix 框架中使用 Payload 的 Local API。它通过 pnpm monorepo 组织了两个应用,workspace 声明见 pnpm-workspace.yaml:
payload子包:Next.js 应用,承载 Payload 管理后台与 Payload 配置定义(集合、数据库适配器等);website子包:Remix 应用,通过payload-app这个 workspace 包导入 payload 配置,从而在服务端 loader/action 中直接调用 Local API,而不是走 REST 接口。
根目录 package.json 提供了两个聚合脚本:
"scripts": {
"dev:payload": "cd payload && pnpm dev",
"dev:website": "cd website && pnpm dev"
}
website 的 package.json 中可以看到关键的依赖关系:payload: "latest" 提供运行时能力,payload-app: "workspace:*" 把 payload 子包的配置以 workspace 依赖形式引入。payload 子包的 package.json 通过 exports 将入口指向 src/index.ts,该文件只做两件事:导出 config(payload 配置)与导出 payload-types 中的全部类型。因此 website 侧一句 import { config, Post } from 'payload-app' 即可同时拿到配置对象与生成的 TypeScript 类型。
开发环境启动(Development)
website 的 README 给出的开发命令为:
npm run dev
在 website 子包的 package.json 中它对应 remix vite:dev,即以 Vite 插件模式启动 Remix 的开发服务器(HMR + 路由约定)。
由于本示例是两个应用协同工作的,完整的启动流程按 examples/remix 的 README 为:
# 1. 复制两边各自的 .env.example 为 .env
cp ./payload/.env.example ./payload/.env
cp ./website/.env.example ./website/.env
# 2. 安装依赖
pnpm install
# 3. 启动 Payload(Next.js 管理后台 + Local API 宿主)
pnpm run dev:payload
# 4. 在另一个终端启动 Remix 网站
pnpm run dev:website
两个 dev 脚本分别在两个终端中运行:payload 侧执行 next dev(payload 子包 scripts),website 侧执行 remix vite:dev。website 的 tsconfig.json 使用 moduleResolution: "Bundler"、target: "ES2022"、noEmit: true,并注明“Vite takes care of building everything, not tsc”,即类型检查与构建职责分离,tsc 只做 typecheck 脚本("typecheck": "tsc")使用。Node 版本要求见 website package.json 的 engines 字段:>=20.0.0。
数据流:Loader/Action 中调用 Local API
website 子应用之所以能消费 Payload 数据,关键在于 app/routes/_index.tsx 中的 loader 与 action 直接导入了 getPayload 与配置:
import { getPayload, PaginatedDocs } from 'payload'
import { config, Post } from 'payload-app'
export const loader = async () => {
const payload = await getPayload({ config })
const posts = await payload.find({ collection: 'posts', sort: 'createdAt' })
return Response.json(posts)
}
action 中则根据表单是否携带隐藏字段 postId 走删除或新建分支,同样通过 payload.delete / payload.create 完成写操作。这说明在 Remix 的 Node 运行时环境中,Payload 的 Local API 与在 Next.js 中一样,是以“同进程调用”方式工作的,无需认证 cookie 或 REST 序列化。
被查询的 posts 集合定义在 payload.config.ts 中:仅含一个必填 title 文本字段,数据库为 mongooseAdapter(连接串来自 DATABASE_URL 环境变量),secret 来自 PAYLOAD_SECRET。该配置还包含一个 onInit 钩子:若 posts 集合为空则创建一条种子文档 'Post 1',因此首次启动后 website 首页列表不会为空。配置中的 typescript.outputFile 指向 payload-types.ts,即 website 侧 Post 类型的来源。
生产构建与 DIY 部署(Deployment)
website 的 README 给出的部署步骤分三段:
1. 构建生产包
npm run build
对应 package.json 中的 remix vite:build,产物输出到 build/ 目录。
2. 以生产模式运行
npm start
对应 remix-serve ./build/server/index.js,即使用 @remix-run/serve 托管服务端 bundle(build/server 内的 Node 服务端入口)。
3. DIY 部署要点
README 指出:如果你熟悉 Node 应用部署,Remix 内置的应用服务器已经可以直接用于生产环境,部署时需要携带 npm run build 的两类产物:
build/server—— 服务端代码 bundle,由 Node 进程执行;build/client—— 客户端静态资源,由 HTTP 服务器直接分发。
即任何可以跑 Node 服务 + 静态文件的托管方案(自建服务器、容器等)均可承载该应用。
Vite 构建配置的关键细节
vite.config.ts 是该子应用构建行为的核心,值得逐条拆解:
export default defineConfig({
plugins: [
remix({
future: {
v3_fetcherPersist: true,
v3_relativeSplatPath: true,
v3_throwAbortReason: true,
v3_singleFetch: true,
v3_lazyRouteDiscovery: true,
},
}),
tsconfigPaths(),
],
ssr: {
external: ['sharp'],
// Reduces Docker image size
noExternal: process.env.NODE_ENV === 'production' ? [/.*/] : [],
},
optimizeDeps: {
exclude: ['sharp', 'file-type'],
},
})
- future flags:启用了全部 5 个 V3 未来特性(
v3_singleFetch等),并在文件顶部通过declare module '@remix-run/node'将v3_singleFetch类型固化为true,保证类型层面与运行时行为一致; ssr.external: ['sharp']:sharp是原生模块且依赖__dirname定位动态库,必须保持 external 而不被打包,否则会在服务端运行时报错(Dockerfile 中的注释也印证了这一点);ssr.noExternal:生产构建时把所有依赖内联进服务端 bundle(正则[/.*/]),开发时则不内联。文件内注释说明其目的是减小 Docker 镜像体积——生产环境无需携带完整的node_modules;optimizeDeps.exclude:开发模式下对sharp、file-type跳过 Vite 依赖预构建,避免原生/动态加载库被错误地转译。
tsconfigPaths() 插件使 Vite 能识别 tsconfig.json 中的 ~/* -> ./app/* 路径别名,与 tsc 类型检查行为对齐。
Tailwind CSS 样式方案(Styling)
website 的 README 在 Styling 一节说明:模板已预配置 Tailwind CSS 作为默认的起步样式方案,你也可以替换为任意其他 CSS 框架。
仓库中的实际配置分三层:
-
postcss.config.js 仅注册了
@tailwindcss/postcss一个插件——这是 Tailwind v4 的接入方式,不再需要tailwind.config.js中的content扫描配置:export default { plugins: { '@tailwindcss/postcss': {}, }, } -
app/tailwind.css 是全局样式入口,通过 v4 风格的
@import 'tailwindcss'引入,并用@theme覆盖--font-sans字体栈(Inter 优先),同时用@apply设置html/body的明暗背景与prefers-color-scheme: dark适配; -
app/root.tsx 中
import './tailwind.css'将其接入文档树,并在<head>中通过LinksFunction声明 Inter 字体的 preconnect 与样式表引用。
页面组件(如 app/routes/_index.tsx)全部使用 Tailwind 原子类编写,并配合 dark: 前缀实现暗色模式(如 text-gray-800 dark:text-gray-100)。
SSR 运行时:entry.server 与 entry.client
website 保留了 Remix 模板完整的服务端渲染约定,理解这两个文件有助于把握 build/server 产物的运行时行为:
- app/entry.server.tsx:默认的服务端入口。它用
isbot判断请求 UA,把流量分为两条渲染路径——爬虫走handleBotRequest(onAllReady时输出完整 HTML,等待所有内容就绪),浏览器走handleBrowserRequest(onShellReady时即可流式输出 HTML shell,后续内容增量填充)。两条路径都使用renderToPipeableStream+PassThrough流式写入响应体,并设置ABORT_DELAY = 5_000毫秒作为渲染中止超时;流式过程中的错误会把状态码改为 500 并记录日志。文件头注释也提示:如果该文件被remix reveal之外的方式改动过,可用npx remix reveal重新生成; - app/entry.client.tsx:客户端水合入口,在
startTransition内调用hydrateRoot,以<StrictMode><RemixBrowser /></StrictMode>完成对 SSR 标记的接管。
这套机制保证了 Payload 数据在 loader 中查询后可以直接出现在首屏 HTML 中,而非等待客户端请求,这也是示例选择 Remix(而非纯 SPA 前端)配合 Local API 的意义所在。
容器化部署参考:Dockerfile 实践
虽然 website 的 README 只描述了 DIY 部署的两类产物,仓库中的 website/Dockerfile 给出了一个可直接参考的多阶段构建落地方式,与前述构建细节一一对应:
FROM node:22.14.0-alpine AS base
FROM base AS builder
RUN apk add --no-cache libc6-compat
RUN npm install -g pnpm@latest-10
COPY . .
ENV NODE_ENV=production
RUN pnpm install --no-frozen-lockfile
RUN pnpm --filter website build
FROM base AS runner
RUN npm install -g @remix-run/serve@latest
COPY --from=builder /app/website/build/ ./build
# Sharp 需单独安装,避免把整个 node_modules 复制进最终镜像
RUN npm install sharp@0.32.6
# Payload 运行时需要能打开配置文件,否则会出现文件打开错误
COPY --from=builder /app/website/package.json ./package.json
COPY --from=builder /app/payload/src/payload.config.ts ./payload.config.ts
ENV PAYLOAD_CONFIG_PATH=./payload.config.ts
EXPOSE 3000
ENV PORT=3000
ENV NODE_ENV=production
CMD HOSTNAME="0.0.0.0" remix-serve build/server/index.js
其中有三处与本文前述内容相互印证、值得注意:
sharp@0.32.6在 runner 阶段单独安装,与 vite.config.ts 中ssr.external: ['sharp']的 external 策略配套——sharp 不能被内联进 bundle,生产运行时以 external 模块形式存在,因此镜像里必须能解析到它,但又不能把 builder 的完整node_modules带进来;- 通过
PAYLOAD_CONFIG_PATH=./payload.config.ts环境变量告知 Payload 运行时配置文件位置,并把payload.config.ts从 builder 阶段复制到 runner 镜像(注释指出这是避免 Payload “file opening errors” 的必要文件); - 最终命令
remix-serve build/server/index.js正是 README 中npm start(remix-serve ./build/server/index.js)的容器等价物,监听 3000 端口。
小结
围绕 website 子应用 这份文档,可以把整个示例的运行链路归纳为一条清晰的主线:
| 环节 | 命令/产物 | 源码依据 |
|---|---|---|
| 依赖安装 | pnpm install(monorepo,workspace 链接 payload-app) |
pnpm-workspace.yaml |
| 本地开发 | pnpm run dev:payload + pnpm run dev:website(即 remix vite:dev) |
根 package.json、website package.json |
| 数据读写 | loader/action 中 getPayload({ config }) 调用 find/create/delete |
app/routes/_index.tsx |
| 生产构建 | remix vite:build → build/server + build/client |
website package.json |
| 生产运行 | remix-serve ./build/server/index.js(Node >= 20,Docker 示例用 Node 22) |
website package.json、Dockerfile |
| 样式 | Tailwind v4:@tailwindcss/postcss + @import 'tailwindcss' + @theme |
postcss.config.js、tailwind.css |
需要说明的适用前提:该示例基于 Remix 2.x(@remix-run/* ^2.15.2)与 Tailwind v4 编写,数据库依赖 MongoDB(mongooseAdapter),DATABASE_URL 与 PAYLOAD_SECRET 需自行配置;它是旧版 website 模板改造的示例(见 examples/remix/README.md 的说明),用于理解“非 Next.js 框架 + Payload Local API”的集成模式,而非 Payload 官方推荐的主流接入路径——仓库中 packages/next 仍是 Next.js 集成的一等公民。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00