뉴스레터 가입 폼

뉴스레터 가입 폼4분 분량

서버 없는 정적 사이트의 이메일 가입 폼

Hugo, Jekyll, Eleventy, Astro 정적 사이트에 뉴스레터 가입 폼을 추가하고, 이메일 도구의 API 키는 페이지에서 빼고, 확인은 도구에서 합니다.

Preet SutharIkiform 창업자

정적 사이트에는 POST할 내 서버가 없으므로 폼은 독자의 브라우저에서 Ikiform으로 바로 POST합니다. 이메일 도구의 API 키는 사이트에서 완전히 빼세요. 브라우저는 공개 Ikiform 엔드포인트만 알고, 다른 곳에 호스팅된 릴레이가 구독자를 추가합니다. 폼은 partial이나 include로 한 번만 작성하고, 위치마다 다른 source를 넘기고, 새로고침 없는 버전을 위해 작은 스크립트 하나를 추가하세요.

이 경우가 다른 점

정적 사이트는 GitHub Pages, Netlify, Cloudflare Pages, S3 버킷 같은 호스트에 있는 파일입니다. 독자가 폼을 제출할 때 내가 작성한 어떤 것도 서버에서 실행되지 않습니다. 그래서 페이지에서 이메일 도구의 API를 호출하는 흔한 지름길은 쓸 수 없습니다. 브라우저가 실행하는 것은 누구나 읽을 수 있기 때문입니다.

이메일 도구 API 키가 구독자 한 명 추가로만 제한되는 경우는 드뭅니다. 구독자를 추가할 수 있는 키는 대개 목록 전체를 읽고, 내보내고, 삭제할 수도 있습니다. 정적 사이트의 JavaScript에 넣으면 모든 방문자에게 배포되고, 거기서 개발자 도구를 여는 누구에게나 전해집니다.

정적 사이트에서 이메일 도구 API 키가 있는 곳페이지 속 키릴레이 속 키정적 페이지JavaScript 속API 키이메일 도구API누구나 호출 가능내 목록누구나 읽을 수 있음정적 페이지공개 엔드포인트만Ikiform스팸 검사, 서명된 웹훅릴레이API 키 보관이메일 도구구독자 추가키전송서명됨키정적 사이트에서 이메일 도구 API 키가 있는 곳페이지 속 키릴레이 속 키정적 페이지JavaScript 속 API 키이메일 도구 API누구나 호출 가능내 목록누구나 읽을 수 있음정적 페이지공개 엔드포인트만Ikiform스팸 검사, 서명된 웹훅릴레이API 키 보관이메일 도구구독자 추가키전송서명됨키
  1. 키가 페이지에 있으면 모든 방문자가 내려받고, 누구나 그 키로 이메일 도구의 API를 호출할 수 있습니다.
  2. 릴레이를 쓰면 페이지는 폼 POST만 받는 Ikiform 엔드포인트만 압니다.
  3. Ikiform이 스팸을 걸러 내고 릴레이로 서명된 웹훅을 보냅니다.
  4. 정적 사이트 밖에 호스팅된 릴레이가 API 키를 가지고 구독자를 추가합니다.
폼 엔드포인트는 공개해도 안전합니다. 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가 표시된 모든 폼을 처리하고, 하나가 성공하면 전부 교체하므로 조금 아래에서 다시 구독하라는 요청을 받지 않습니다. 기본 레이아웃에서 한 번만 로드하세요.

static/newsletter.js
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 규칙은 웹훅 문서에 있습니다.

정적 사이트는 별도 도메인에 배포 미리보기가 있는 경우가 많습니다. Allowed domains에 없는 도메인에서 온 POST는 스팸으로 표시되거나 거부되므로, 테스트하는 동안 미리보기 도메인을 추가하거나 실제 도메인과 localhost에서 테스트하세요.

사이트가 Content Security Policy를 보낸다면

많은 정적 호스트가 Netlify와 Cloudflare Pages의 _headers 같은 파일로 헤더를 설정하게 해 줍니다. 사이트가 Content-Security-Policy를 보낸다면, 정책이 Ikiform을 허용하지 않는 한 브라우저가 폼을 막습니다. form-action은 일반 POST를, connect-src는 스크립트의 fetch를 다룹니다.

_headers
Content-Security-Policy: default-src 'self'; form-action 'self' https://ikiform.com; connect-src 'self' https://ikiform.com

막힌 POST는 브라우저 콘솔에 CSP 오류로 나타나고 Ikiform에 도달하지 않으므로 대시보드에도 아무것도 나타나지 않습니다.

작동 확인하기

  1. Allowed domains에 실제 도메인과 localhost를 추가하세요.
  2. 사이트를 빌드하고 글 페이지를 여세요. 두 폼이 모두 있고, 페이지 소스에서 각각 고유한 source 값을 가집니다.
  3. 글 폼에서 내 주소로 구독하세요. 두 폼 모두 받은편지함을 확인하라는 문구로 바뀝니다.
  4. JavaScript를 끄고 푸터에서 구독하세요. /subscribed/로 이동합니다.
  5. 대시보드에서 Submissions를 여세요. 두 가입의 source가 post와 footer로 표시됩니다.
  6. 빌드된 사이트에서 이메일 도구의 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을 설정하세요.

질문

막히는 단계가 있거나 내 상황에 맞는 가이드가 필요하다면 이메일을 보내세요: hello@ikiform.com.