How I Built a Dynamic Contact Form on a Static Website Using Cloudflare Pages

Learn how I combined Cloudflare Pages, GitHub and D1 database to make a fast, free and fully functional contact form

Posted by Hüseyin Sekmenoğlu on August 19, 2025 Backend Development Blogging Tooling & Productivity

🌐 Exploring Faster and Free Hosting

I wanted a faster and free hosting solution for my website so I started exploring my options. After evaluating several platforms I decided to use Cloudflare Pages.

The best part is I connected my GitHub repository to Cloudflare Pages. Now every time I make a change in the repository the website refreshes automatically. This makes updates seamless and keeps deployment friction-free.


πŸ—οΈ Backend Setup

Originally my website was built on Umbraco using a clean template. To make it compatible with static hosting I used a custom website exporter to export the entire site into static HTML.

Then I pushed the exported website to my GitHub repository. Thanks to the GitHub integration with Cloudflare Pages the static site automatically updates every time I push changes.


πŸ“¬ Adding a Dynamic Contact Form

Even though the site is static I wanted a dynamic contact form to collect messages. For this I used Cloudflare Pages Functions together with D1 database (Cloudflare’s serverless SQLite).

Here is the workflow I implemented:

  1. Contact Form Frontend

    • Built a simple HTML form with name, email and message fields

    • Used JavaScript fetch() to POST the data to a serverless function

  2. Serverless Function (functions/api/submit.js)

    • Receives POST requests from the form

    • Inserts the data into a D1 database table called contacts

    • Returns a success message to the user

  3. Database Setup

    • Created the contacts table using a migration SQL file

CREATE TABLE IF NOT EXISTS contacts (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  email TEXT NOT NULL,
  message TEXT NOT NULL,
  created_at TEXT NOT NULL
);
  • Applied the migration using wrangler d1 migrations apply mydb

  1. Viewing Submissions

    • Built a simple dashboard page (submissions.html) to fetch and display submissions from D1

    • Added a delete button for each entry to manage messages directly from the dashboard

  2. Security

    • Optional: secure the dashboard with a secret token or Cloudflare Access


1. Contact Form (Frontend)

index.html

<form id="contact-form">
  <input type="text" name="name" required placeholder="Your Name">
  <input type="email" name="email" required placeholder="Your Email">
  <textarea name="message" required placeholder="Your Message"></textarea>
  <button type="submit">Send</button>
</form>

<script>
document.getElementById("contact-form").addEventListener("submit", async (e) => {
  e.preventDefault();
  const data = Object.fromEntries(new FormData(e.target).entries());

  const res = await fetch("/api/submit", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data)
  });

  alert(res.ok ? "Message sent!" : "Error sending message");
  if (res.ok) e.target.reset();
});
</script>

2. Function to Save Submissions

functions/api/submit.js

export async function onRequestPost({ request, env }) {
  const data = await request.json();
  const { name, email, message } = data;

  if (!name || !email || !message) {
    return new Response("Missing fields", { status: 400 });
  }

  await env.DB.prepare(
    "INSERT INTO contacts (name, email, message, created_at) VALUES (?, ?, ?, datetime('now'))"
  ).bind(name, email, message).run();

  return new Response("Saved", { status: 200 });
}

3. Function to List Submissions

functions/api/list.js

export async function onRequestGet({ env }) {
  try {
    const { results } = await env.DB.prepare(
      "SELECT id, name, email, message, created_at FROM contacts ORDER BY created_at DESC"
    ).all();

    return new Response(JSON.stringify(results), {
      headers: { "Content-Type": "application/json" }
    });
  } catch (err) {
    return new Response("Error: " + err.message, { status: 500 });
  }
}

4. Dashboard Page

submissions.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Contact Submissions</title>
  <style>
    body { font-family: sans-serif; margin: 20px; }
    table { border-collapse: collapse; width: 100%; margin-top: 20px; }
    th, td { border: 1px solid #ccc; padding: 8px; }
    th { background: #f2f2f2; }
  </style>
</head>
<body>
  <h1>📬 Contact Submissions</h1>
  <table id="submissions-table">
    <thead>
      <tr><th>ID</th><th>Name</th><th>Email</th><th>Message</th><th>Date</th></tr>
    </thead>
    <tbody></tbody>
  </table>

  <script>
  async function loadSubmissions() {
    const res = await fetch("/api/list");
    const data = await res.json();

    const tbody = document.querySelector("#submissions-table tbody");
    tbody.innerHTML = "";
    data.forEach(row => {
      const tr = document.createElement("tr");
      tr.innerHTML = `
        <td>${row.id}</td>
        <td>${row.name}</td>
        <td>${row.email}</td>
        <td>${row.message}</td>
        <td>${row.created_at}</td>
      `;
      tbody.appendChild(tr);
    });
  }

  loadSubmissions();
  </script>
</body>
</html>

5. Database Setup

Migration: migrations/0001_init.sql

CREATE TABLE IF NOT EXISTS contacts (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL,
  email TEXT NOT NULL,
  message TEXT NOT NULL,
  created_at TEXT NOT NULL
);

Apply it:

npx wrangler d1 migrations apply mydb

6. Wrangler Config

wrangler.toml

[[d1_databases]]
binding = "DB"             # must match env.DB
database_name = "mydb"
database_id = "<your-database-id>"

Also in Cloudflare Dashboard β†’ Pages β†’ Settings β†’ Functions β†’ D1 Databases β†’ Add Binding (DB β†’ mydb).


7. Deploy

npx wrangler pages deploy ./dist

⚑ Result

  • The website is now fully static, hosted on Cloudflare Pages for speed and free hosting

  • The contact form is dynamic, storing data in a Cloudflare D1 database

  • Submissions can be viewed and deleted via a dashboard

  • Every change in GitHub automatically redeploys the site


πŸ“Œ Summary

By combining static hosting, serverless functions and D1 database I was able to make a fast, free and fully functional website with dynamic capabilities.

This approach is perfect for anyone who wants the performance of a static website but still needs backend functionality like forms and data storage