From b7692551e15f0b7371f7632e15bac58a128ed9f0 Mon Sep 17 00:00:00 2001 From: bot-implementer Date: Tue, 25 Aug 2026 10:45:12 +0000 Subject: [PATCH 1/6] feat: add contact page with name, email, and message fields --- contact.html | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 contact.html diff --git a/contact.html b/contact.html new file mode 100644 index 0000000..d7d37fc --- /dev/null +++ b/contact.html @@ -0,0 +1,44 @@ + + + + + + Contact | My Personal Blog + + + + +
+

Contact

+

Use the form below to get in touch — submitting it opens your mail client with the details pre-filled.

+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + + + From b99c1b27d02f427a435fa76cb53046352069d696 Mon Sep 17 00:00:00 2001 From: bot-implementer Date: Tue, 25 Aug 2026 10:45:17 +0000 Subject: [PATCH 2/6] feat: add pure mailto URL builder --- js/mailto.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 js/mailto.js diff --git a/js/mailto.js b/js/mailto.js new file mode 100644 index 0000000..d353119 --- /dev/null +++ b/js/mailto.js @@ -0,0 +1,15 @@ +/** + * Build a `mailto:` URL from an address, subject, and body. + * Subject and body are URL-encoded so the visitor's mail client + * opens pre-filled with the submitted values. + * + * @param {{to?: string, subject?: string, body?: string}} options + * @returns {string} + */ +export function buildMailtoUrl({ to = "", subject = "", body = "" } = {}) { + const params = new URLSearchParams(); + if (subject) params.set("subject", subject); + if (body) params.set("body", body); + const query = params.toString(); + return query ? `mailto:${to}?${query}` : `mailto:${to}`; +} From 0d76bc01fe728a0c4d498fca8fbc5b0bda6cbf3b Mon Sep 17 00:00:00 2001 From: bot-implementer Date: Tue, 25 Aug 2026 10:45:23 +0000 Subject: [PATCH 3/6] feat: wire contact form submit to open mail client via mailto --- js/contact.js | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 js/contact.js diff --git a/js/contact.js b/js/contact.js new file mode 100644 index 0000000..a94a157 --- /dev/null +++ b/js/contact.js @@ -0,0 +1,72 @@ +import { buildMailtoUrl } from "./mailto.js"; + +/** The address the contact form messages are sent to. */ +export const MAILTO_TARGET = "hello@example.com"; + +/** + * Collect form field values by name. + * 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 {{name: string, email: string, message: string}} + */ +export function readFormValues(form) { + const values = {}; + for (const el of form.querySelectorAll("[name]")) { + values[el.name] = el.value; + } + return values; +} + +/** + * Build the mailto: URL for the contact form from the entered values. + * + * @param {{name: string, email: string, message: string}} values + * @param {string} [to] + * @returns {string} + */ +export function buildContactMailto(values, to = MAILTO_TARGET) { + const subject = `Contact form submission from ${values.name || "someone"}`; + const body = [ + `Name: ${values.name || ""}`, + `Email: ${values.email || ""}`, + "", + "Message:", + values.message || "", + ].join("\n"); + return buildMailtoUrl({ to, subject, body }); +} + +/** + * Handle a contact form submit: build the mailto: URL and send the + * visitor to it so their mail client opens pre-filled. + * + * @param {HTMLFormElement} form + * @param {{to?: string, navigate?: (url: string) => void}} [options] + * @returns {string} the mailto: URL that was navigated to + */ +export function handleContactSubmit( + form, + { to = MAILTO_TARGET, navigate = (url) => { window.location.href = url; } } = {} +) { + const url = buildContactMailto(readFormValues(form), to); + navigate(url); + return url; +} + +/** + * Wire the submit handler onto a contact form. + * @param {HTMLFormElement} form + */ +export function initContactForm(form) { + form.addEventListener("submit", (event) => { + event.preventDefault(); + handleContactSubmit(form); + }); +} + +// Browser-only wiring — guarded so this module stays importable in Node tests. +if (typeof window !== "undefined" && typeof document !== "undefined") { + const form = document.getElementById("contact-form"); + if (form) initContactForm(form); +} From 2f5c522cc39da1a6f703baa53775bb9fa5cabd3c Mon Sep 17 00:00:00 2001 From: bot-implementer Date: Tue, 25 Aug 2026 10:45:28 +0000 Subject: [PATCH 4/6] test: unit tests for the mailto URL builder --- tests/mailto.test.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/mailto.test.js diff --git a/tests/mailto.test.js b/tests/mailto.test.js new file mode 100644 index 0000000..30b8ad9 --- /dev/null +++ b/tests/mailto.test.js @@ -0,0 +1,33 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { buildMailtoUrl } from "../js/mailto.js"; + +test("builds a plain mailto URL when there is no subject or body", () => { + assert.equal(buildMailtoUrl({ to: "ada@example.com" }), "mailto:ada@example.com"); +}); + +test("includes subject and body as URL-encoded query parameters", () => { + const url = buildMailtoUrl({ + to: "owner@example.com", + subject: "Hello, Ada!", + body: "Line one\nLine two & ", + }); + + assert.ok(url.startsWith("mailto:owner@example.com?")); + + const params = new URL(url).searchParams; + assert.equal(params.get("subject"), "Hello, Ada!"); + assert.equal(params.get("body"), "Line one\nLine two & "); +}); + +test("omits empty subject and body", () => { + const url = buildMailtoUrl({ to: "owner@example.com", subject: "", body: "" }); + assert.equal(url, "mailto:owner@example.com"); +}); + +test("encodes spaces and special characters so the mail client opens pre-filled", () => { + const url = buildMailtoUrl({ to: "o@example.com", subject: "a b", body: "x&y=z" }); + const params = new URL(url).searchParams; + assert.equal(params.get("subject"), "a b"); + assert.equal(params.get("body"), "x&y=z"); +}); From d94e769806063d452a6d136f59473b9647d28981 Mon Sep 17 00:00:00 2001 From: bot-implementer Date: Tue, 25 Aug 2026 10:45:36 +0000 Subject: [PATCH 5/6] test: component + unit tests for the contact page --- tests/contact.test.js | 109 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 tests/contact.test.js diff --git a/tests/contact.test.js b/tests/contact.test.js new file mode 100644 index 0000000..d2a848d --- /dev/null +++ b/tests/contact.test.js @@ -0,0 +1,109 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { + MAILTO_TARGET, + buildContactMailto, + handleContactSubmit, + readFormValues, +} from "../js/contact.js"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const contactHtml = readFileSync(join(root, "contact.html"), "utf8"); +const indexHtml = readFileSync(join(root, "index.html"), "utf8"); + +/** Minimal stand-in for a DOM form (has querySelectorAll("[name]")). */ +function fakeForm(values) { + return { + querySelectorAll(selector) { + assert.equal(selector, "[name]"); + return Object.entries(values).map(([name, value]) => ({ name, value })); + }, + }; +} + +// --------------------------------------------------------------------------- +// Component tests: the contact page renders the three required fields +// --------------------------------------------------------------------------- + +test("contact page renders a form with name, email, and message fields", () => { + assert.match(contactHtml, /]*id="contact-form"/); + assert.match(contactHtml, /]*type="text"[^>]*id="name"[^>]*name="name"/); + assert.match(contactHtml, /]*type="email"[^>]*id="email"[^>]*name="email"/); + assert.match(contactHtml, /]*id="message"[^>]*name="message"/); +}); + +test("all three contact form fields are required", () => { + assert.match(contactHtml, /]*id="name"[^>]*required/); + assert.match(contactHtml, /]*id="email"[^>]*required/); + assert.match(contactHtml, /]*id="message"[^>]*required/); +}); + +test("contact page loads the module that wires form submission", () => { + assert.match(contactHtml, /]*type="module"[^>]*src="js\/contact\.js"/); +}); + +// --------------------------------------------------------------------------- +// Component tests: the page is linked from site navigation +// --------------------------------------------------------------------------- + +test("home page nav links to the contact page", () => { + assert.match(indexHtml, /Contact<\/a>/); + assert.match(indexHtml, /class="nav-links"/); +}); + +test("contact page nav marks the contact link as the current page", () => { + assert.match(contactHtml, /]*aria-current="page"[^>]*>Contact<\/a>/); +}); + +// --------------------------------------------------------------------------- +// Unit tests: submitting the form opens the mail client pre-filled +// --------------------------------------------------------------------------- + +test("buildContactMailto pre-fills subject and body from entered values", () => { + const url = buildContactMailto( + { name: "Ada Lovelace", email: "ada@example.com", message: "Hi there!" }, + "owner@example.com" + ); + + assert.ok(url.startsWith("mailto:owner@example.com?")); + + const params = new URL(url).searchParams; + assert.equal(params.get("subject"), "Contact form submission from Ada Lovelace"); + assert.match(params.get("body"), /Name: Ada Lovelace/); + assert.match(params.get("body"), /Email: ada@example\.com/); + assert.match(params.get("body"), /Hi there!/); +}); + +test("buildContactMailto defaults to the configured mailto target", () => { + const url = buildContactMailto({ name: "A", email: "a@b.c", message: "m" }); + assert.ok(url.startsWith(`mailto:${MAILTO_TARGET}?`)); +}); + +test("readFormValues collects name, email, and message", () => { + const values = readFormValues(fakeForm({ name: "N", email: "E", message: "M" })); + assert.deepEqual(values, { name: "N", email: "E", message: "M" }); +}); + +test("handleContactSubmit navigates the visitor to the built mailto URL", () => { + const form = fakeForm({ + name: "Grace Hopper", + email: "grace@example.com", + message: "Hello!", + }); + const navigated = []; + + const url = handleContactSubmit(form, { + to: "owner@example.com", + navigate: (target) => navigated.push(target), + }); + + assert.equal(navigated.length, 1); + assert.ok(navigated[0].startsWith("mailto:owner@example.com?")); + assert.equal(navigated[0], url); + + const params = new URL(navigated[0]).searchParams; + assert.match(params.get("body"), /Grace Hopper/); +}); From fdc306ac338f31065ab991a4ab4e79b36882de2c Mon Sep 17 00:00:00 2001 From: bot-implementer Date: Tue, 25 Aug 2026 10:45:45 +0000 Subject: [PATCH 6/6] feat: link the contact page from the home page navigation --- index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/index.html b/index.html index dfa2216..28aa417 100644 --- a/index.html +++ b/index.html @@ -12,6 +12,7 @@ My Blog