表单

不用后端也能用的表单

给静态网站上的表单加一个属性,每一条提交都会出现在你的仪表盘里。无需 JavaScript,不用嵌入小部件,不用第三方服务,填表的人也不必注册任何东西。

Its markup, styles and deploy script are on GitHub — copy it, change the fields, redeploy.

index.html
<form harvis-form="contact">
  <input name="email" type="email" required>
  <textarea name="message"></textarea>
  <button>Send</button>
</form>
how it works

三步,其中一步是重新部署

没有表单生成器要学,也没有接口地址要复制。这个属性就是全部设置。

  1. 01

    加上属性

    在 HTML 里给任意表单写上 harvis-form。如果网站上不止一个表单,就给它起个名字。action 和 method 不用写 —— 页面被送出时会自动补上。

  2. 02

    重新部署

    把文件夹再拖一次,或者运行 npx harvis。无需配置,不用粘贴密钥,也不必先去仪表盘里打开什么开关。

  3. 03

    读收到的内容

    提交会出现在仪表盘里你的网站下面,最新的在最前。点开一行就能读完整内容,也可以把全部导出成 CSV。

the markup

整件事就是两个属性

harvis 会在表单离开服务器时改写它,所以你写的页面依然是你写的那个页面。

你写的
<form harvis-form="contact"
      data-harvis-redirect="/thanks.html">
  <input name="email" type="email" required>
  <button>Send</button>
</form>
访客拿到的
<form harvis-form="contact"
      data-harvis-redirect="/thanks.html"
      action="/__harvis/form/contact" method="post">
  <input type="hidden" name="_harvis_redirect" value="/thanks.html">
  <input type="text" name="_harvis_hp" tabindex="-1" aria-hidden="true" style="…">
  <input name="email" type="email" required>
  <button>Send</button>
</form>

这两个属性

harvis-form
把该表单标记为需要收集。属性值就是它的名字,这样同一个网站上的多个表单不会混在一起;不写值就会收集到“default”名下。
data-harvis-redirect
可选。访客提交后要跳转到的、你网站上的页面。任何指向站外的地址都会被忽略。
frameworks

如果表单是 JavaScript 生成的,就自己把端点写进去

harvis 会在页面离开服务器时填上 action。而只有在你的 bundle 跑完后才存在的表单,此刻还不在页面里,自然也就无从填起 —— 于是由你写出那次改写本会写下的内容。它仍旧是一次普通的 HTML post,本页其余内容照样适用。

  • React
  • Vue
  • Svelte
  • Angular
ContactForm.jsx
<form action="/__harvis/form/contact" method="post">
  <input name="email" type="email" required />
  <textarea name="message" />

  {/* optional — where to land after sending */}
  <input type="hidden" name="_harvis_redirect" value="/thanks" />

  {/* optional — the decoy harvis would have added */}
  <input
    type="text"
    name="_harvis_hp"
    tabIndex={-1}
    autoComplete="off"
    aria-hidden="true"
    style={{ position: "absolute", left: "-9999px", opacity: 0 }}
  />

  <button>Send</button>
</form>
action + method
就是 harvis 本会写下的端点,再加上 post。最后一段是表单的名字 —— 小写字母、数字和短横线;其他情况一律归到「default」下。
_harvis_redirect
可选,是 data-harvis-redirect 的手写版本。必须是你自己站点上的路径;指向站外的一律忽略。
_harvis_hp
可选,也是你自己写表单唯一会失去的东西:抓机器人的诱饵。用移出屏幕而不是 display:none,并让它保持为空 —— 凡是填了它的提交都会被丢弃。
harvis-form
别加它。这个属性的意思是请 harvis 来写 action,而你刚刚已经自己写好了。

让浏览器去提交

不要 preventDefault,不要 fetch。响应是一个浏览器自己会跟随的重定向,正是这一点让表单在脚本失效时依然可用。

预渲染也算 JavaScript

如果你的构建把表单写进了 HTML,之后应用又在浏览器里接管它,那也照样把这些字段写上 —— harvis 插入的那些不在你的组件里,水合时可能会被丢掉。

ai agents

Hand it to the agent that wrote your site

If an AI built the site, it can wire the forms up too. Copy the brief below into whatever has your project open — Claude Code, Cursor, Copilot, the chat you built the page in — and it will find the forms you already have and convert them. It covers both cases on this page, so you don't have to know which one you're in.

paste this into your ai agent
Wire the forms in this project up to harvis.dev (https://harvis.dev), which collects form submissions for static sites that have no backend. Read all of this before editing anything.

HOW IT WORKS
harvis serves every page of a site it hosts, so it can take a form post on the way past. Marking a <form> with the harvis-form attribute is the whole integration: as the page is served, harvis rewrites that tag to action="/__harvis/form/<name>" method="post", inserts a hidden honeypot field, and turns data-harvis-redirect into a hidden field. The form then posts natively, same-origin, with no JavaScript, no API key and no third-party service. Submissions appear in the site owner's dashboard (https://harvis.dev/dashboard) and are emailed to them.

STEP 1 — FIND THE FORMS
Look for every <form> in the project: .html files, and templates or components if a framework is in use. Skip search boxes and any form that posts to an API this project owns and expects a reply from. For each remaining form, decide which case it is:
- CASE A — the form is in the HTML that gets deployed: plain HTML, or a generator/framework that renders it at build time. This is the common case.
- CASE B — the form only exists once JavaScript has run: a React/Vue/Svelte/Angular component mounted in the browser, or markup a script writes. harvis rewrites the served HTML, so a form that is not in it yet is never rewritten. A form that is prerendered and then hydrated is CASE B too, because the fields harvis inserts are not in the component tree and hydration can discard them.

STEP 2A — CASE A: ADD THE ATTRIBUTE
- Add harvis-form="<name>" to the opening <form> tag. Name it for what it is — contact, signup, feedback. The name must match ^[a-z0-9][a-z0-9_-]{0,39}$ or it is collected under "default". Two forms on one site should not share a name unless they should share one list of submissions.
- Delete that form's existing action and method attributes. harvis overwrites both, so leaving them there only misleads whoever reads the file next.
- Optional: add data-harvis-redirect="/thanks.html" to choose where the visitor lands after sending. It must be a path on this same site and the file must exist in the deploy; anything pointing off-site is ignored. Without it, visitors get a plain harvis confirmation page.
- Do not add a honeypot, _harvis_hp or _harvis_redirect by hand — harvis inserts them, and a second copy is a bug.

STEP 2B — CASE B: WRITE THE ENDPOINT YOURSELF
- Set action="/__harvis/form/<name>" and method="post" on the form, and do NOT add harvis-form: that attribute is a request to rewrite, and here you have written the rewrite yourself.
- Optional redirect: a hidden input named _harvis_redirect whose value is a path on this site.
- Optional honeypot, since nothing will add one for you: an empty text input named _harvis_hp with tabindex -1, autocomplete off, aria-hidden true, positioned off-screen with position:absolute;left:-9999px rather than display:none.
- Let the browser submit it: no onSubmit handler, no preventDefault, no fetch. The reply is a 303 that the browser follows on its own, and intercepting it is what breaks the form when scripts fail.

STEP 3 — IN BOTH CASES
- Every field to be collected needs a name attribute; an input without one is never submitted. Those names become the column headings the owner reads, so prefer name, email and message over field1.
- Remove what is left of any other form service: a Formspree, Getform, Basin or FormSubmit action URL, Netlify's data-netlify attribute and its hidden form-name input, a Web3Forms access_key input, and any handler that POSTed the form somewhere else.
- Keep the client-side validation as it is. required, type="email", minlength and the rest all still work.
- File inputs are dropped: submissions are stored as text and files are not kept. If a form has one, say so rather than leaving it in silently.
- Add no script, SDK, key or config file. There is nothing to install.
- Limits, worth mentioning if a form is likely to meet one: 30 fields per submission, 5,000 characters per field, 64 KB per submission, 60 submissions per site an hour, 20 per visitor an hour, and the newest 1,000 per site are kept.

STEP 4 — DEPLOY AND REPORT
- The attribute only does anything on a served page, so deploy the site again: run npx harvis from the site folder, or tell the user to drag the folder onto https://harvis.dev/drop.
- Then tell the user which files changed, the name you gave each form, and that submissions arrive at https://harvis.dev/dashboard and by email — noting that a site deployed anonymously has no owner to email until it is claimed with the claim link.
- Suggest they send one test submission through the live site.

What it tells the agent to do

  • Find every form in the project and leave the search boxes and API calls alone.
  • Add harvis-form with a sensible name, and strip the action and method that harvis replaces anyway.
  • Write the endpoint by hand instead when the form only exists after JavaScript runs.
  • Clear out whatever the last form service left behind, and flag a file upload as something that won't be kept.
  • Redeploy, then tell you what changed and where the submissions land.

Or leave it in the repo

The same text works as a file — save it as AGENTS.md, CLAUDE.md or a project rule and an agent reads it on its own, so the next form someone adds is collected without anyone asking.

included

你会得到什么

下面这些默认全部开启,而且都没有设置页面可调。

  • 无需 JavaScript

    表单就按 HTML 一直以来的方式提交。脚本被拦截时能用,在慢速手机上能用,在只渲染文本的浏览器里也能用。

  • 自动过滤垃圾信息

    每个表单都会附带一个真人看不见的诱饵字段,凡是填了它的提交都会被悄悄丢弃。频率限制则会限定单个访客、以及单个网站每小时能发送多少条。

  • 想要多少表单都行

    给它们起名字 —— 同一个网站上的联系表单和注册表单各有各的列表,可以分别筛选和导出。

  • 你自己的致谢页面

    把表单指向你自己写的页面,提交后访客就会到那里。不设置的话,他们会看到一个朴素的确认页面。

  • 随时导出

    点一下按钮就能拿到 CSV,任何人提交过的每个字段都有一列 —— 所以中途多加了一个问题的表单,打开后仍然是一张完整的表。

  • 不会追踪任何人

    没有追踪脚本,没有 cookie,页面里也没有第三方。访客的地址在存储前会先做哈希,而且只是为了让频率限制能区分两次提交。

limits

各项限制

对联系表单来说很宽裕,又紧到让一个盯上你网站的脚本填不满你的收件箱。

每条提交的字段数
30
单个字段长度
5,000 个字符
单条提交大小
64 KB
每个网站的提交量
每小时 60 条
每位访客的提交量
每小时 20 条
每个网站保留
1,000 条,最新的在前
faq

常见问题

在给静态网站加表单之前,大家通常会问这些。

什么是静态表单?

就是放在没有自己服务器的网站上的表单。通常这意味着表单没地方可以提交,所以静态网站往往要借用第三方表单服务。harvis 本来就在为你网站的每个页面提供服务,因此可以顺手把提交接下来。

我需要会写代码吗?

你只要会在一行 HTML 里加一个词就行。如果网站是 AI 写的,直接让它给表单加上 harvis-form 属性 —— 它知道怎么做,因为 harvis 面向 AI 助手发布的说明里就是这么写的。

关掉 JavaScript 还能用吗?

能。正是为此才这样设计。表单执行的是普通的 HTML 提交,浏览器随后跟随一次跳转 —— 和 JavaScript 出现之前表单的工作方式完全一样。

我能继续用自己的接口或别的表单服务吗?

能 —— 不加这个属性就行。没有 harvis-form 的表单会原样送出,连 action 都不动。而对加了这个属性的表单,harvis 会替换掉 action,因为加上属性正是你在说明提交该送到哪里。

它能配合 React、Vue 或 Svelte 吗?

能,只是多一步。如果表单已经在你构建产出的 HTML 里,加个属性就够了。如果它要等 JavaScript 跑完才出现,那就改在组件里写上 action="/__harvis/form/contact" 和 method="post" —— 同一个端点,同一个面板,提交依然由浏览器完成。

访客可以上传附件吗?

暂时还不行。被收集的表单上的文件输入框对访客仍然可用,但文件不会被保存 —— 只保留文本字段。

如果我删掉网站,提交会怎么样?

会一起没了。提交属于网站而不属于某次部署,所以重新部署会保留它们,删除网站则会彻底移除。想留着的话,先导出一份 CSV。

把网站放到线上试试看

发布是免费的,大约两秒钟。给表单加上属性,重新部署,然后给自己发一条测试消息。

把网站放到线上