diff --git a/js/newsletter-config.js b/js/newsletter-config.js
new file mode 100644
index 0000000..ac8f9f4
--- /dev/null
+++ b/js/newsletter-config.js
@@ -0,0 +1,17 @@
+/**
+ * Newsletter signup configuration.
+ *
+ * The blog is a static site with no build step, so the serverless endpoint
+ * and its API token are configured here — edit this file when deploying.
+ *
+ * Security note: a real API token must NEVER be committed to the repository.
+ * The committed default below is intentionally empty; the deployer fills in
+ * the token at deploy time (and only then). The page logic reads the token
+ * from this module and never hardcodes one.
+ */
+
+/** The serverless endpoint the signup form POSTs to. */
+export const NEWSLETTER_ENDPOINT = "https://example.com/api/newsletter-subscribers";
+
+/** API token for the endpoint. Empty by default — set at deploy time. */
+export const NEWSLETTER_API_TOKEN = "";
diff --git a/js/newsletter.js b/js/newsletter.js
new file mode 100644
index 0000000..fbe176e
--- /dev/null
+++ b/js/newsletter.js
@@ -0,0 +1,132 @@
+/**
+ * Newsletter signup — posts the visitor's email to a configured serverless
+ * endpoint using the API token from `newsletter-config.js`.
+ *
+ * Pure-ish by design (no DOM, injectable fetch), so every behaviour is unit
+ * testable in Node; the browser wiring at the bottom is guarded accordingly.
+ */
+import {
+ NEWSLETTER_API_TOKEN,
+ NEWSLETTER_ENDPOINT,
+} from "./newsletter-config.js";
+
+/** User-safe message shown when the signup cannot be completed. */
+export const NEWSLETTER_ERROR_MESSAGE =
+ "Sorry, the newsletter signup isn't available right now. Please try again later.";
+
+/** Confirmation message shown after a successful signup. */
+export const NEWSLETTER_SUCCESS_MESSAGE = "Thanks for subscribing!";
+
+/** Simple email shape check; the HTML5 `type="email"` input is the primary gate. */
+export function isValidEmail(value) {
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(value).trim());
+}
+
+/**
+ * Read the trimmed email value from a form.
+ * Works with a real DOM form and with the minimal fake form used in tests.
+ *
+ * @param {{querySelectorAll(selector: string): ArrayLike<{name: string, value: string}>}} form
+ * @returns {string}
+ */
+export function readFormEmail(form) {
+ for (const el of form.querySelectorAll("[name]")) {
+ if (el.name === "email") return el.value.trim();
+ }
+ return "";
+}
+
+/**
+ * POST the email to the serverless endpoint with the API token.
+ *
+ * The token travels in the `Authorization` header; it is never put in the
+ * body, the URL, or any message. A missing token fails fast with a user-safe
+ * error and no network call. Any non-2xx response (invalid token, endpoint
+ * error) and any network failure map to the same generic message, so the
+ * secret and endpoint internals are never surfaced to the visitor.
+ *
+ * @param {string} email
+ * @param {{endpoint?: string, token?: string, fetchImpl?: typeof fetch}} [options]
+ * @returns {Promise<{ok: boolean, message: string}>}
+ */
+export async function submitNewsletterSignup(
+ email,
+ { endpoint = NEWSLETTER_ENDPOINT, token = NEWSLETTER_API_TOKEN, fetchImpl = fetch } = {}
+) {
+ if (!token) {
+ return { ok: false, message: NEWSLETTER_ERROR_MESSAGE };
+ }
+
+ let response;
+ try {
+ response = await fetchImpl(endpoint, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify({ email }),
+ });
+ } catch {
+ return { ok: false, message: NEWSLETTER_ERROR_MESSAGE };
+ }
+
+ if (!response.ok) {
+ return { ok: false, message: NEWSLETTER_ERROR_MESSAGE };
+ }
+ return { ok: true, message: NEWSLETTER_SUCCESS_MESSAGE };
+}
+
+/**
+ * Handle a newsletter form submit: validate the email, POST it, and surface
+ * the result (confirmation or user-safe error) through `setStatus`.
+ *
+ * @param {HTMLFormElement} form
+ * @param {{endpoint?: string, token?: string, fetchImpl?: typeof fetch, setStatus?: (message: string, kind: "success"|"error") => void}} [options]
+ * @returns {Promise<{ok: boolean, message: string}>}
+ */
+export async function handleNewsletterSubmit(
+ form,
+ {
+ endpoint = NEWSLETTER_ENDPOINT,
+ token = NEWSLETTER_API_TOKEN,
+ fetchImpl = fetch,
+ setStatus = defaultSetStatus,
+ } = {}
+) {
+ const email = readFormEmail(form);
+ if (!isValidEmail(email)) {
+ setStatus(NEWSLETTER_ERROR_MESSAGE, "error");
+ return { ok: false, message: NEWSLETTER_ERROR_MESSAGE };
+ }
+
+ const result = await submitNewsletterSignup(email, { endpoint, token, fetchImpl });
+ setStatus(result.message, result.ok ? "success" : "error");
+ return result;
+}
+
+/** Default status renderer — puts the message into `#newsletter-status`. */
+function defaultSetStatus(message, kind) {
+ const el = document.getElementById("newsletter-status");
+ if (el) {
+ el.textContent = message;
+ el.dataset.kind = kind;
+ }
+}
+
+/**
+ * Wire the submit handler onto a newsletter form.
+ * @param {HTMLFormElement} form
+ */
+export function initNewsletterForm(form, options = {}) {
+ form.addEventListener("submit", (event) => {
+ event.preventDefault();
+ handleNewsletterSubmit(form, options);
+ });
+}
+
+// Browser-only wiring — guarded so this module stays importable in Node tests.
+if (typeof window !== "undefined" && typeof document !== "undefined") {
+ const form = document.getElementById("newsletter-form");
+ if (form) initNewsletterForm(form);
+}
diff --git a/newsletter.html b/newsletter.html
new file mode 100644
index 0000000..3bf2871
--- /dev/null
+++ b/newsletter.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+ Newsletter | My Personal Blog
+
+
+
+
+
+
+
+
Newsletter
+
Occasional updates on new posts — no spam, unsubscribe anytime. Enter your email to subscribe.