快速回答:Next.js 用戶端元件、Astro 孤島、Hugo 純靜態頁面,都能用一個託管端點存下每一筆表單提交——不寫 API 路由、不維護伺服器。
前端又轉回靜態了。Next.js 在邊緣渲染,Astro 預設不發一行 JavaScript,Hugo 把一疊 Markdown 編譯成純 HTML——快、可快取、託管便宜。然後你想起來,每個真實網站遲早都需要一樣東西:表單。只要有訪客要提交點什麼,那個問題就又回來了:資料該存到哪?
直覺答案是「加個 API 路由」。但對一個靜態渲染的網站來說,這個答案會悄悄拆掉整個架構——你突然要接手一個 serverless 函式、它的冷啟動、它的環境變數,還有背後的資料庫。其實有個更輕的做法:頁面維持原樣靜態,表單把資料 POST 給一個託管端點,每一筆提交都替你存好。
一個端點,三種提交形態
發佈任意一個 FormLM 表單,你就得到一個 HTTP 端點。同樣的資料,它照你的技術棧習慣的形態接收:
- fetch 送出 JSON——適合 React 用戶端元件、Vue,或任何已經 hydrate 的孤島。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 孤島:不 hydrate,直接原生 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>Email <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 預算——這正是孤島架構的原意:hydrate 花在互動發生的地方,而不是鋪滿整份文件。
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 開發。
