快速回答:Next.js 客户端组件、Astro 孤岛、Hugo 纯静态页面,都能用一个托管接口存下每一条表单提交——不写 API 路由、不维护服务器。
前端又转回静态了。Next.js 在边缘渲染,Astro 默认不发一行 JavaScript,Hugo 把一堆 Markdown 编译成纯 HTML——快、可缓存、托管便宜。然后你会想起来,每个真实网站迟早都需要一样东西:表单。只要有访客需要给你提交点什么,那个问题就回来了:数据该存到哪?
本能答案是"加个 API 路由"。但对一个静态渲染的站点来说,这个答案会悄悄拆掉整个架构——你突然要负责一个 serverless 函数、它的冷启动、它的环境变量,还有背后的数据库。其实有个更轻的做法:页面保持原样的静态,表单把数据 POST 给一个托管接口,提交记录替你存好。
一个接口,三种提交形态
发布任意一个 FormLM 表单,你就得到一个 HTTP 接口。同样的数据,它按你的技术栈习惯的形态接收:
- fetch 提交 JSON——适合 React 客户端组件、Vue,或任何已经水合的孤岛。POST
{"data": {...}},完事。 - 原生表单 POST——普通
action+method="POST",指向/form路径。零 JavaScript,Astro 静态标记和 Hugo 产物原样可用。 - 自带文档的 schema——接口 URL 后面加
?help,返回这份接口自己的 Markdown 字段文档。这也是你交给 AI 编程助手去集成的入口。
读写两端都开了 CORS,浏览器可以从任何来源直接提交——包括本地 file:// 打开的页面。不需要代理,不需要同源绕路。
Next.js:客户端组件跳过 API 路由
在 App Router 里,走一方 API 路由的表单要绕一圈服务端。当"后端"的全部职责只是"把这条对象存下来",这一圈就是纯开销。把组件标成客户端组件,直接 POST 到接口:
'use client';
import { useState } from 'react';
type Feedback = { name: string; rating: number; note?: string };
const ENDPOINT = 'https://formlm.me/api/v3/share/<your-token>'; // 发布后的 Data API 地址
export default function FeedbackForm() {
const [done, setDone] = useState(false);
async function submit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const f = new FormData(e.currentTarget);
const data: Feedback = {
name: String(f.get('name')),
rating: Number(f.get('rating')),
note: f.get('note') ? String(f.get('note')) : undefined,
};
await fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data }),
});
setDone(true);
}
if (done) return <p>提交成功,感谢反馈。</p>;
return (
<form onSubmit={submit}>
<input name="name" required />
<input name="rating" type="number" min={1} max={5} required />
<textarea name="note" />
<button>提交</button>
</form>
);
}
Feedback 类型就是你与接口之间的全部契约。这个页面不依赖任何服务端运行时:照样预渲染,照样部署到任何静态托管,交互全部发生在浏览器里。如果你确实保留了服务端组件,同一个 POST 放进 server action 也一样成立——区别只是 fetch 这一行写在哪儿。
Astro 孤岛:不水合,直接原生 POST
Astro 默认只发 HTML,这意味着项目里最便宜的表单就是一个普通 HTML 表单。action 指向 /form 路径,每个字段名都会成为存下记录里的一个键:
<form action="https://formlm.me/api/v3/share/<your-token>/form" method="POST" autocomplete="off"> <label>称呼 <input name="name" required /></label> <label>邮箱 <input name="email" type="email" required /></label> <textarea name="message" required></textarea> <input type="hidden" name="page" value="/contact" /> <!-- 蜜罐字段:真人永远不会填它 --> <input name="website" style="display:none" tabindex="-1" /> <button type="submit">提交</button> </form>
想要提交后不打断页面就给出成功提示?只把表单升级成一个孤岛(套一层很小的 React 或 Svelte 组件,标上 client:visible),换成上面的 fetch 写法。页面其余部分保持零 JS 预算——这正是孤岛架构的本意:水合花在有交互的地方,而不是铺满整个文档。
Hugo 与一切纯静态站点
Hugo、Eleventy、预渲染的 Gatsby、手写的纯 HTML——同一个原生表单 POST 就是通用答案,因为它不需要浏览器本来就会的事情以外的任何东西。页面生成一次,表单就永远可用,构建环节里没有后端什么事。把它放进 layouts/partials/form.html,整个集成就这一句话。
实操建议:每个表单都留一个蜜罐字段。机器人会填隐藏字段,真人从不会往看不见的地方打字。因为未知字段会自动长出自己的列,多一个 website 字段毫无成本——而存下的记录里这一列一旦有值,就是一眼可辨的垃圾提交,读数据时顺手过滤即可。
让 schema 跟着页面一起演进
静态站点是持续迭代的:下个月给联系表单加个 company 字段,或者给反馈组件加个量表。在 FormLM 接口上,"改后端"根本不是一个任务。提交一个接口没见过的新键,第一次提交时它就变成新的一列。想让这个特性用得安全,三个习惯:
- 给 payload 立类型,但每个键都可选——TypeScript 类型是写给作者看的,接口照单全收。
- 读的时候防御一点——旧记录天然没有新列,看板和脚本里把缺失的键一律当 null。
- 用
?help复核 schema——接口会返回当前字段清单的 Markdown 文档,你和你的 Agent 都不会对着过期的 schema 写代码。
不跳转的成功态
原生表单 POST 之后,浏览器会停在接口的响应页——对必须无 JS 运行的页面,这是可以接受的取舍。其余场景把访客留在页面上:await 这次 fetch,用一段感谢态换掉表单,在注意力最高的时刻给出下一步动作。对一个静态站点来说,这一小段反馈就是用户能看到的全部"后端体验"——值得花这两行代码。
"站点转回静态之后,后端问题并没有消失——只是变小了。而小正是重点:一个会存下每条提交的 URL,比一个函数、一条队列、一个数据库,划算太多了——对那些服务端需求只有一张表单的海量站点来说。"
🛠️ 把接口接进你的框架
表单长大的过程里,开发者专区替你保持事实是新的:
- 每个发布后的表单一个 URL——JSON、原生 HTML POST,加
?help自述文档 - 未知字段自动建列——payload 加键即加列,没有迁移这一步
- 数据后台 + CSV 导出——给人看的,接口查询留给脚本
- 读写全开 CORS——本地、磁盘文件、生产环境都直连
✅ 重点结论
- 静态渲染的栈照样需要表单存储——接口方案让页面保持原来的静态
- Next.js:客户端组件直接 POST JSON,API 路由整个跳过,页面照常预渲染
- Astro 与 Hugo:原生表单 POST 指向 /form,零 JavaScript、零构建负担
- 蜜罐字段几乎免费(未知键自动成列),让垃圾提交在读数据时一眼可见
- schema 演进是特性:payload 里加个键,第一次提交接口就认识了它
- 把访客留在页面上——await 之后用感谢态换掉表单
常见问题
FormLM 能给我的 React 或 Next.js 应用当后端吗?
能。在客户端组件里把 JSON 以 {"data": {...}} 的形态直接 POST 到发布后的表单接口。CORS 已开启,不需要 API 路由和代理,页面照常预渲染、照常部署到任何静态托管。
纯静态、完全不用 JavaScript 的站点可以吗?
可以。把普通 HTML 表单的 action 指向接口的 /form 路径原生提交,字段名就是记录里的键。这正是 Hugo、Eleventy 以及零 JS 的 Astro 页面的标准做法。
怎么防垃圾提交?
最便宜的一道防线是隐藏蜜罐字段:机器人会填,真人永远不会。未知键会自动成列,所以蜜罐字段随记录一起存下——这一列有值的记录一眼就是垃圾,数据视图和查询结果里都好过滤。
加字段不想重新部署后端,怎么办?
在 payload 里直接把新键交上来就行。自动建列机制会在第一次提交时把陌生键变成新的一列;接口 URL 加 ?help 还能拿到当前字段的 Markdown 清单,你的代码——人或 Agent——都不会对着过期 schema 开发。
