Formulir pendaftaran newsletter7 menit baca
Buat formulir pendaftaran newsletter di Astro
Formulir newsletter Astro yang tetap di halaman prerender: Astro Action memeriksa persetujuan, mengirim ke Ikiform, dan endpoint menambahkan pembaca ke MailerLite.
Biarkan halaman Astro Anda di-prerender dan taruh formulir pendaftaran di komponen footer. Tanpa JavaScript, formulir mengirim ke satu halaman /newsletter on demand yang menjalankan Astro Action; dengan JavaScript, script kecil memanggil action yang sama dan pembaca tidak pernah meninggalkan artikel. Action memvalidasi email dan persetujuan dengan astro/zod, mengirim ke backend formulir Anda dengan secret key, dan endpoint API meneruskan pendaftaran yang terverifikasi ke MailerLite.
Tab Astro di panduan utama memakai Action di halaman yang dirender on demand. Sebagian besar situs Astro adalah blog dan dokumentasi yang setiap halamannya di-prerender, dan formulir footer harus ada di semuanya. Halaman ini menjaga halaman-halaman itu tetap statis dan tetap menjalankan pendaftaran di server. Key dan allowed domains ada di panduan dokumentasi Astro.
Apa yang diubah halaman statis
Dokumentasi Astro menyatakan halaman harus dirender on demand untuk memanggil action lewat atribut action formulir. Artikel yang di-prerender tidak bisa menangani POST itu, karena tidak ada server di belakangnya. Ada dua jalan memutar, dan build ini memakai keduanya:
| Jalur | Kapan berjalan | Yang dilihat pembaca |
|---|---|---|
Script memanggil actions.subscribe() | JavaScript termuat, kasus yang biasa | Baris status di bawah formulir berubah. Mereka tetap di artikel |
Formulir mengirim ke /newsletter?_action=... | JavaScript mati atau belum termuat | Halaman /newsletter on demand, dengan hasil atau error-nya |
Hanya /newsletter dan endpoint webhook yang berjalan on demand. Setiap artikel tetap file statis, dan adapter menambahkan server function untuk kedua route itu.
- Dengan JavaScript, script footer menghentikan kiriman normal dan memanggil action dengan data formulir.
- Action memvalidasi input, mengirim ke Ikiform, dan mengembalikan data atau error ke script, yang memperbarui baris status.
- Tanpa JavaScript, browser mengirim formulir ke halaman /newsletter on demand dengan action di query string.
- Astro menjalankan handler action yang sama, dan halaman membaca hasilnya dengan getActionResult lalu menampilkannya.
Setup proyek
src/
actions/index.ts
components/SignupForm.astro
layouts/Base.astro
pages/
newsletter.astro
api/ikiform.ts
astro.config.mjs
.envInstal adapter dengan astro add vercel, astro add netlify, atau astro add cloudflare. Deklarasikan secret dengan astro:env alih-alih membaca import.meta.env: schema menggagalkan build saat variabel tidak ada, dan import yang sama bekerja di setiap adapter, termasuk runtime Cloudflare.
import { defineConfig, envField } from "astro/config";
import vercel from "@astrojs/vercel";
export default defineConfig({
adapter: vercel(),
env: {
schema: {
IKIFORM_SECRET_KEY: envField.string({ context: "server", access: "secret" }),
IKIFORM_WEBHOOK_SECRET: envField.string({ context: "server", access: "secret" }),
MAILERLITE_API_KEY: envField.string({ context: "server", access: "secret" }),
MAILERLITE_GROUP_ID: envField.string({ context: "server", access: "secret" }),
},
},
});IKIFORM_SECRET_KEY=ik_sk_your_secret_key
IKIFORM_WEBHOOK_SECRET=whsec_your_webhook_secret
MAILERLITE_API_KEY=your_mailerlite_token
MAILERLITE_GROUP_ID=123456789Buat secret key di API keys dengan scope submissions:write, dan tempel definisi field dari pilar di tab Fields formulir.
Bangun
Action-nya
import { ActionError, defineAction } from "astro:actions";
import { IKIFORM_SECRET_KEY } from "astro:env/server";
import { z } from "astro/zod";
export const server = {
subscribe: defineAction({
accept: "form",
input: z.object({
email: z.email({ error: "Enter an email address we can send the newsletter to." }),
first_name: z.string().trim().max(80).nullish(),
consent: z.boolean().refine((ticked) => ticked, { error: "Tick the box so we know you want the emails." }),
source: z.enum(["footer", "post", "newsletter-page"]).catch("footer"),
_gotcha: z.string().nullish(),
}),
handler: async (input, context) => {
const origin = context.request.headers.get("origin");
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Authorization: `Bearer ${IKIFORM_SECRET_KEY}`,
...(origin ? { Origin: origin } : {}),
},
body: JSON.stringify({
email: input.email,
first_name: input.first_name || undefined,
consent: true,
source: input.source,
_gotcha: input._gotcha ?? "",
}),
}).catch(() => null);
const result = await response?.json().catch(() => null);
if (result?.ok) return { subscribed: true };
throw new ActionError({
code: response?.status === 429 ? "TOO_MANY_REQUESTS" : "BAD_REQUEST",
message: result?.error?.fields?.email?.[0] ?? result?.error?.message ?? "We couldn't reach the list. Try again.",
});
},
}),
};accept: "form"membuat Astro mem-parsingFormData. Checkbox melewati koersi boolean, jadi dicentang bernilaitruedan tidak dicentang bernilaifalse, yang diubahrefinemenjadi pesan persetujuan.- Input teks kosong tiba sebagai
null, jadi field opsional memakainullish(), bukanoptional(). sourcekembali kefooteruntuk nilai apa pun di luar daftar, jadi kiriman rekayasa tidak bisa mengarang penempatan.- Baris
Originmeneruskan origin browser, karena Ikiform mengenali ujilocalhostgratis dari header itu danfetchmilik server tidak mengirimnya. - Error Ikiform menjadi
ActionError.429tetap bermakna sebagaiTOO_MANY_REQUESTS.
IKIFORM_SECRET_KEY di environment variable khusus server.Komponen footer
---
import { actions } from "astro:actions";
interface Props {
source?: "footer" | "post" | "newsletter-page";
}
const { source = "footer" } = Astro.props;
const target = new URL("/newsletter", Astro.url);
target.search = actions.subscribe.queryString;
---
<form class="signup" method="POST" action={target.pathname + target.search}>
<label>Email <input name="email" type="email" required autocomplete="email" /></label>
<label>First name (optional) <input name="first_name" maxlength="80" autocomplete="given-name" /></label>
<label>
<input name="consent" type="checkbox" required />
Email me the newsletter. I can unsubscribe at any time.
</label>
<input type="hidden" name="source" value={source} />
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<p class="signup-status" role="status"></p>
<button>Subscribe</button>
</form>
<script>
import { actions, isInputError } from "astro:actions";
for (const form of document.querySelectorAll<HTMLFormElement>("form.signup")) {
const status = form.querySelector(".signup-status") as HTMLElement;
const button = form.querySelector("button") as HTMLButtonElement;
form.addEventListener("submit", async (event) => {
event.preventDefault();
button.disabled = true;
status.textContent = "Subscribing";
const { error } = await actions.subscribe(new FormData(form));
button.disabled = false;
if (!error) {
form.replaceChildren(status);
status.textContent = "Almost done. Open the email we sent you and confirm your address.";
return;
}
const fields = isInputError(error) ? error.fields : {};
status.textContent = fields.email?.[0] ?? fields.consent?.[0] ?? error.message;
});
}
</script>queryStringadalah cara terdokumentasi untuk menyusun URL action. Mengaturnya pada URL/newslettermengarahkan kiriman tanpa JavaScript ke halaman on demand alih-alih halaman statis asalnya.- Astro mem-bundle
<script>milik komponen sekali, berapa kali pun komponen itu muncul. Loop menghubungkan setiap salinan, seperti formulir footer dan satu lagi di akhir artikel. isInputErrormemisahkan pesan per field dari zod dari penolakan Ikiform, yang tiba sebagai pesan biasa.
Halaman on demand
---
import { actions, isInputError } from "astro:actions";
import SignupForm from "../components/SignupForm.astro";
import Base from "../layouts/Base.astro";
export const prerender = false;
const result = Astro.getActionResult(actions.subscribe);
const fields = isInputError(result?.error) ? result.error.fields : {};
const problem = fields.email?.[0] ?? fields.consent?.[0] ?? result?.error?.message;
---
<Base title="Newsletter">
<h1>The monthly newsletter</h1>
{result?.data?.subscribed ? (
<p role="status">Almost done. Open the email we sent you and confirm your address.</p>
) : (
<>
{problem && <p role="alert">{problem}</p>}
<SignupForm source="newsletter-page" />
</>
)}
</Base>Astro menampilkan hasil action hanya untuk POST itu. Mengunjungi halaman lagi akan menghapusnya, dan itu tidak masalah untuk pendaftaran. Jika Anda lebih suka me-redirect ke URL terima kasih, dokumentasi Astro menunjukkan pola berbasis sesi dengan getActionContext di middleware.
Teruskan subscriber ke MailerLite
Endpoint ini menerima webhook bertanda tangan dari Ikiform dan menambahkan setiap pendaftaran ke grup MailerLite sebagai unconfirmed, sehingga MailerLite mengirim konfirmasinya sendiri saat Double opt-in for API and integrations aktif. Endpoint membaca body sebagai teks lebih dulu karena tanda tangan mencakup byte persisnya. Endpoint mengirim setiap pendaftaran sebagai unconfirmed, yang cocok untuk daftar baru. Jika daftar Anda sudah punya pembaca terkonfirmasi, gunakan relay di panduan MailerLite: relay itu mencari alamatnya lebih dulu, jadi pembaca terkonfirmasi yang mendaftar lagi mempertahankan statusnya.
import type { APIRoute } from "astro";
import { IKIFORM_WEBHOOK_SECRET, MAILERLITE_API_KEY, MAILERLITE_GROUP_ID } from "astro:env/server";
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";
export const prerender = false;
export const POST: APIRoute = async ({ request }) => {
let event;
try {
event = await verifyWebhook({ payload: await request.text(), headers: request.headers, secret: IKIFORM_WEBHOOK_SECRET });
} catch (error) {
if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
throw error;
}
if (event.type !== "submission.created") return new Response(null, { status: 204 });
const { email, first_name, consent } = event.data.fields;
if (consent !== true || typeof email !== "string") return new Response("Skipped", { status: 200 });
const answer = await fetch("https://connect.mailerlite.com/api/subscribers", {
method: "POST",
headers: {
Authorization: `Bearer ${MAILERLITE_API_KEY}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
email,
fields: typeof first_name === "string" && first_name ? { name: first_name } : {},
groups: [MAILERLITE_GROUP_ID],
status: "unconfirmed",
}),
});
if (answer.status === 429) return new Response("MailerLite rate limit", { status: 503, headers: { "retry-after": "60" } });
if (!answer.ok) return new Response(`MailerLite answered ${answer.status}`, { status: 502 });
return new Response(null, { status: 204 });
};security.checkOrigin milik Astro, aktif secara default, menolak form post lintas situs ke route on demand. Webhook Ikiform berupa application/json, tipe yang tidak terkena pemeriksaan itu, jadi endpoint menerimanya apa adanya. Tambahkan URL di Webhooks dan salin secret whsec_ ke IKIFORM_WEBHOOK_SECRET.
Honeypot dan pemeriksaan manusia
Input _gotcha diteruskan dan Ikiform menyimpan yang terisi sebagai spam, dengan jawaban sukses dan tanpa webhook, jadi tidak pernah sampai ke MailerLite. Secret key melewati pemeriksaan Turnstile Ikiform. Jika bot lolos, Anda punya dua pilihan: verifikasi token Turnstile di handler, seperti yang ditunjukkan panduan Turnstile, atau hapus action dan kirim dari browser dengan @ikiform/sdk, yang mempertahankan pemeriksaan Ikiform, allowed domains, dan batas per pengunjung serta tidak butuh adapter sama sekali.
Deploy
| Host | Yang harus dilakukan |
|---|---|
| Vercel | @astrojs/vercel mengubah kedua route on demand menjadi function. Tambahkan keempat variabel di Environment Variables. |
| Netlify | @astrojs/netlify melakukan hal yang sama dengan Netlify Functions. Tambahkan variabel di pengaturan environment situs. |
| Cloudflare | @astrojs/cloudflare menargetkan Workers; adapter ini tidak lagi mendukung Pages. Atur secret dengan wrangler secret put, dan gunakan .dev.vars secara lokal. Beri Worker domain kustom, karena Ikiform menolak URL webhook workers.dev. |
Uji gratis di localhost
Workspace tanpa paket menerima 50 kiriman uji per bulan dari localhost. Jalankan astro dev dan Origin yang diteruskan adalah http://localhost:4321, jadi setiap pendaftaran dihitung sebagai uji. Coba kedua jalur: sekali secara normal, dan sekali dengan JavaScript dimatikan untuk melihat halaman /newsletter. Untuk menguji endpoint, ekspos lewat tunnel dengan URL HTTPS; Ikiform tidak akan mengirim ke localhost.
Pastikan berfungsi
- Jalankan
astro builddan pastikan artikel Anda masih tercantum sebagai prerender di output, dengan hanya/newsletterdan/api/ikiformyang on demand. - Berlangganan dari footer sebuah artikel. Baris status berubah dan URL tidak.
- Matikan JavaScript dan berlangganan lagi. Anda mendarat di
/newsletterdengan baris periksa-inbox. - Kirim dengan kotak tidak dicentang dari dev tools. Pesan persetujuan tampil dan tidak ada yang disimpan.
- Buka Submissions di dashboard. Kiriman baru ada di sana dengan setiap field yang Anda kirim.
- Jika Anda menambahkan webhook, buka endpoint-nya di Webhooks. Pengiriman menampilkan
succeededdengan jawaban dari server Anda. - Buka MailerLite. Subscriber ada di grup sebagai unconfirmed, dan email konfirmasi ada di inbox Anda.
Hal yang sering menjebak
| Masalah | Yang harus dilakukan |
|---|---|
| Kiriman tanpa JavaScript menampilkan 404 atau halaman statis | Formulir mengirim ke halaman prerender asalnya. Susun action dari /newsletter ditambah queryString, dan pertahankan prerender = false di halaman itu. |
| Hanya formulir pertama di halaman yang berfungsi | Script memakai querySelector untuk satu formulir. Astro menjalankannya sekali per halaman, jadi lakukan loop atas setiap form.signup. |
| Build gagal karena environment variable tidak ada | astro:env memeriksa schema saat build. Atur variabel di host, atau di .env secara lokal. |
402 plan_required pada uji lokal | Kiriman tidak punya origin localhost. Periksa baris Origin di handler, dan buka situs di localhost. |
| Kiriman ada di dashboard tetapi tidak ada email atau webhook yang datang | Kiriman itu disimpan sebagai spam. Spam tidak mengirim apa pun. Buka di filter Spam untuk melihat alasannya, dan tandai sebagai bukan spam untuk mengirim email dan webhook-nya. |
Pertanyaan
Jika dipanggil dari script klien, bisa, selama proyek punya adapter: action berjalan di route on demand-nya sendiri. Form post HTML biasa butuh halaman on demand sebagai tujuan, itulah sebabnya build ini mengirimnya ke /newsletter.
Hanya untuk versi sisi server di halaman ini. Situs Astro yang sepenuhnya statis bisa mengirim formulir langsung dari browser ke Ikiform, yang mempertahankan pemeriksaan manusia Ikiform dan tidak butuh server.
Action tidak bisa membedakan bot dari pembaca: Ikiform memberi keduanya jawaban sukses yang sama. Webhook-nya hanya terpicu untuk pendaftaran yang lolos pemeriksaan spam, dan gangguan MailerLite kemudian hanya menunda subscriber lewat percobaan ulang webhook alih-alih menggagalkan kiriman pengunjung.
Tidak, tidak ada paket gratis. Pengujian gratis: Anda bisa mengirim 50 kiriman uji per bulan dari localhost sebelum membayar. Untuk go live, Pro mulai $6/bln untuk 500 kiriman, atau Anda bisa bayar sekali untuk 10.000 kiriman seharga $49 atau 50.000 kiriman seharga $149, dengan kredit yang tidak pernah kedaluwarsa.
Mentok di salah satu langkah, atau ingin panduan untuk situasi Anda? Email hello@ikiform.com.