뉴스레터 가입 폼4분 분량
서버 없는 정적 사이트의 이메일 가입 폼
Hugo, Jekyll, Eleventy, Astro 정적 사이트에 뉴스레터 가입 폼을 추가하고, 이메일 도구의 API 키는 페이지에서 빼고, 확인은 도구에서 합니다.
정적 사이트에는 POST할 내 서버가 없으므로 폼은 독자의 브라우저에서 Ikiform으로 바로 POST합니다. 이메일 도구의 API 키는 사이트에서 완전히 빼세요. 브라우저는 공개 Ikiform 엔드포인트만 알고, 다른 곳에 호스팅된 릴레이가 구독자를 추가합니다. 폼은 partial이나 include로 한 번만 작성하고, 위치마다 다른 source를 넘기고, 새로고침 없는 버전을 위해 작은 스크립트 하나를 추가하세요.
이 경우가 다른 점
정적 사이트는 GitHub Pages, Netlify, Cloudflare Pages, S3 버킷 같은 호스트에 있는 파일입니다. 독자가 폼을 제출할 때 내가 작성한 어떤 것도 서버에서 실행되지 않습니다. 그래서 페이지에서 이메일 도구의 API를 호출하는 흔한 지름길은 쓸 수 없습니다. 브라우저가 실행하는 것은 누구나 읽을 수 있기 때문입니다.
이메일 도구 API 키가 구독자 한 명 추가로만 제한되는 경우는 드뭅니다. 구독자를 추가할 수 있는 키는 대개 목록 전체를 읽고, 내보내고, 삭제할 수도 있습니다. 정적 사이트의 JavaScript에 넣으면 모든 방문자에게 배포되고, 거기서 개발자 도구를 여는 누구에게나 전해집니다.
- 키가 페이지에 있으면 모든 방문자가 내려받고, 누구나 그 키로 이메일 도구의 API를 호출할 수 있습니다.
- 릴레이를 쓰면 페이지는 폼 POST만 받는 Ikiform 엔드포인트만 압니다.
- Ikiform이 스팸을 걸러 내고 릴레이로 서명된 웹훅을 보냅니다.
- 정적 사이트 밖에 호스팅된 릴레이가 API 키를 가지고 구독자를 추가합니다.
폼은 한 번만 작성하기
뉴스레터 폼은 여러 템플릿에 나타납니다. 푸터, 각 글의 끝, 뉴스레터 페이지입니다. source 값을 받는 partial로 만들면 각 위치가 스스로를 기록합니다. 마크업은 생성기마다 같고, source의 템플릿 문법만 다릅니다.
<form data-newsletter action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
<input name="email" type="email" required autocomplete="email" placeholder="you@example.com" aria-label="Email" />
<label><input name="consent" type="checkbox" value="true" required /> Email me the newsletter. I can unsubscribe at any time.</label>
<input type="hidden" name="source" value="{{ .source | default "footer" }}" />
<input type="hidden" name="_redirect" value="https://example.com/subscribed/" />
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
<button>Subscribe</button>
<p role="status" hidden></p>
</form>그런 다음 위치 이름과 함께 템플릿에 넣으세요.
{{ partial "newsletter.html" (dict "source" "post") }}- Astro에서는
<Newsletter source="post" />를 쓰세요. 정적 Astro 빌드는 Actions를 실행할 수 없으므로 컴포넌트가 다른 것들처럼 Ikiform으로 바로 POST합니다. - Ikiform에서 폼의 필드 정의에
source를 최대 길이가 짧은 텍스트로 추가하세요. 필드를 정의하면 정의에 없는 입력은unknown_field로 거부됩니다. _redirect는 폼의 Allowed domains 중 하나에 있는https:URL이어야 합니다. 독자에게 받은편지함을 확인하라고 안내하는/subscribed/페이지를 만드세요.
페이지의 모든 폼을 위한 스크립트 하나
글 페이지에는 뉴스레터 폼이 두 개 있을 수 있습니다. 글 뒤에 하나, 푸터에 하나입니다. 이 스크립트는 data-newsletter가 표시된 모든 폼을 처리하고, 하나가 성공하면 전부 교체하므로 조금 아래에서 다시 구독하라는 요청을 받지 않습니다. 기본 레이아웃에서 한 번만 로드하세요.
for (const form of document.querySelectorAll("form[data-newsletter]")) {
const status = form.querySelector('[role="status"]');
const button = form.querySelector("button");
form.addEventListener("submit", async (event) => {
event.preventDefault();
button.disabled = true;
status.hidden = false;
status.textContent = "Subscribing";
try {
const response = await fetch(form.action, {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});
const result = await response.json();
if (result.ok) {
for (const other of document.querySelectorAll("form[data-newsletter]")) {
other.replaceChildren(Object.assign(document.createElement("p"), {
textContent: "Almost done. Check your inbox for a confirmation link.",
}));
}
return;
}
if (response.status === 429 && response.headers.has("Retry-After")) {
status.textContent = "Too many tries from this network. Wait a minute and try again.";
} else {
const errors = result.error.fields ?? {};
status.textContent = errors.email?.[0] ?? errors.consent?.[0] ?? result.error.message;
}
} catch {
form.submit();
return;
}
button.disabled = false;
});
}- 엄격한 네트워크 필터 뒤 등에서
fetch가 실패하면 스크립트는form.submit()으로 대체합니다. 일반 방식으로 POST하고/subscribed/로 이동합니다. 429에는 별도 메시지가 있습니다. 한 사무실이나 캠퍼스 네트워크의 독자는 IP를 공유하므로 함께 방문자당 한도에 걸릴 수 있습니다.- 폼은 JavaScript가 꺼져 있어도 동작합니다. 스크립트는 응답을 보여 주는 방식만 바꿉니다.
JavaScript가 꺼진 독자는 /subscribed/로 이동하므로, 그 페이지가 다른 사람에게 스크립트가 하는 일을 대신하게 하세요. 확인 이메일이 가고 있다고 말하고, 찾아야 할 발신자 이름을 알려 주고, 스팸함을 언급하고, 블로그로 돌아가는 링크를 두세요. 생성기에서 다른 페이지처럼 만드는 일반 페이지이며, 나중에 이메일 도구의 확인 링크가 가리킬 곳이기도 합니다.
릴레이를 둘 곳
릴레이에는 코드를 실행하는 공개 HTTPS URL이 필요합니다. 정적 호스트가 이를 제공할 수도, 아닐 수도 있습니다.
| 정적 호스트 | 릴레이 선택지 |
|---|---|
| GitHub Pages, S3 | 함수 없음. 커스텀 도메인의 Cloudflare Worker나 자동화 도구를 쓰세요 |
| Netlify | 사이트 도메인의 Netlify Function, 또는 위의 아무것이나 |
| Cloudflare Pages | 커스텀 도메인의 별도 Worker. Ikiform은 pages.dev와 workers.dev URL을 거부합니다 |
| Vercel | 내 도메인의 함수. 정적 출력 옆에 API 라우트를 둘 수 있습니다 |
이메일 도구별 릴레이 코드는 Kit, MailerLite, beehiiv 페이지에 있습니다. 코드를 전혀 쓰지 않으려면 웹훅을 Zapier나 n8n으로 보내고 이메일 도구 액션을 쓰세요. URL 규칙은 웹훅 문서에 있습니다.
localhost에서 테스트하세요.사이트가 Content Security Policy를 보낸다면
많은 정적 호스트가 Netlify와 Cloudflare Pages의 _headers 같은 파일로 헤더를 설정하게 해 줍니다. 사이트가 Content-Security-Policy를 보낸다면, 정책이 Ikiform을 허용하지 않는 한 브라우저가 폼을 막습니다. form-action은 일반 POST를, connect-src는 스크립트의 fetch를 다룹니다.
Content-Security-Policy: default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com막힌 POST는 브라우저 콘솔에 CSP 오류로 나타나고 Ikiform에 도달하지 않으므로 대시보드에도 아무것도 나타나지 않습니다.
작동 확인하기
- Allowed domains에 실제 도메인과
localhost를 추가하세요. - 사이트를 빌드하고 글 페이지를 여세요. 두 폼이 모두 있고, 페이지 소스에서 각각 고유한
source값을 가집니다. - 글 폼에서 내 주소로 구독하세요. 두 폼 모두 받은편지함을 확인하라는 문구로 바뀝니다.
- JavaScript를 끄고 푸터에서 구독하세요.
/subscribed/로 이동합니다. - 대시보드에서 Submissions를 여세요. 두 가입의
source가post와footer로 표시됩니다. - 빌드된 사이트에서 이메일 도구의 API 키를 검색하세요. 없습니다.
주의할 점
| 문제 | 해결 방법 |
|---|---|
| 대시보드에 아무것도 없고 콘솔에 CSP 오류가 있음 | Content-Security-Policy의 form-action과 connect-src에 https://ikiform.com을 추가하세요. |
source에 대해 unknown_field와 함께 422 | 폼의 필드 정의에 source를 추가하거나 hidden 입력을 제거하세요. |
403 origin_not_allowed, 또는 모든 POST가 스팸으로 분류됨 | 페이지의 도메인이 폼의 Allowed domains에 없습니다. 도메인을 추가하고, 로컬 테스트용으로 localhost도 추가하세요. 등록된 도메인의 하위 도메인은 허용됩니다. |
| 브라우저가 내 페이지가 아니라 Ikiform의 감사 페이지로 이동합니다 | _redirect는 도메인이 Allowed domains에 있고 https: URL일 때만 따릅니다. 도메인을 추가하거나, 대신 폼 설정에서 Redirect after submit을 설정하세요. |
질문
네. 폼은 브라우저에서 Ikiform 같은 폼 백엔드로 POST하고, 백엔드가 각 가입을 저장하고 걸러 냅니다. 다른 곳에 호스팅된 릴레이나 자동화 도구가 이메일 도구로 넘기므로 정적 사이트 자체는 서버 코드를 실행하지 않습니다.
하지 마세요. 페이지 JavaScript에 든 키는 모든 방문자에게 보이고, 이메일 도구 키는 대개 목록 전체를 읽거나 바꿀 수 있습니다. 키는 사이트 밖에서 실행되는 릴레이에 두세요.
네. 폼은 일반 HTML이고 Ikiform으로 POST합니다. GitHub Pages는 릴레이를 실행할 수 없으므로 커스텀 도메인의 Cloudflare Worker에 호스팅하거나 Zapier, n8n 같은 자동화 도구를 쓰세요.
이메일 도구입니다. Ikiform은 가입한 사람에게 절대 이메일을 보내지 않습니다. 릴레이가 독자를 미확인으로 추가하면 도구가 자체 확인 링크를 보냅니다.
막히는 단계가 있거나 내 상황에 맞는 가이드가 필요하다면 이메일을 보내세요: hello@ikiform.com.