Skip to content

Connect contact form to Google Sheets via Apps Script#43

Open
Copilot wants to merge 3 commits intomainfrom
copilot/connect-front-page-form-to-google-sheet
Open

Connect contact form to Google Sheets via Apps Script#43
Copilot wants to merge 3 commits intomainfrom
copilot/connect-front-page-form-to-google-sheet

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 31, 2026

Replaces the Formspree integration with a direct Google Apps Script Web App POST, and expands the form with lead/address fields that land in a managed Google Sheet. Includes anti-abuse hardening and accessibility improvements.

Changes

index.html

  • Adds method="post" action="YOUR_GOOGLE_APPS_SCRIPT_URL" as a progressive-enhancement fallback for no-JS environments
  • New fields: Company/Organization, Address, City, State (with pattern="[A-Za-z]{2}"), Zip
  • Inline #form-status div with role="status" aria-live="polite" aria-atomic="true" for screen-reader announcements
  • Hidden honeypot field (hp) positioned off-screen via .form-honeypot CSS; aria-hidden, tabindex="-1", autocomplete="off"

form-handler.js (new)

  • Intercepts submit, serialises fields + ISO timestamp, POSTs via fetch with mode: 'no-cors'
  • SCRIPT_URL guard: detects placeholder/unconfigured URL and shows an explicit "form not configured" error instead of a silent failed fetch
  • Optimistic success UX — opaque response is expected with no-cors; network failures still surface an error message

google-apps-script/Code.gs (new)

  • doPost(e) handler: auto-creates a frozen-header Leads tab on first run, appends one row per submission: Timestamp | Name | Email | Company | Address | City | State | Zip | Message
  • Honeypot check: silently discards submissions where the hidden hp field is non-empty
  • Required-field guard: rejects submissions missing name or email
  • Rate limiting: per-email cap of 5 submissions/hour via CacheService; normalises + aliases so variants share one bucket
  • Formula injection sanitization: sanitizeCell() prefixes values starting with =, +, -, @, |, or % with a tab so Sheets stores them as plain text
  • Concurrent write safety: LockService.getScriptLock() serialises simultaneous POSTs; timeout returns a user-friendly error

styles.css

  • Contact form widened to 640 px
  • .form-row--thirds for the 3-column City/State/Zip row
  • .form-optional label variant; .form-status--success / .form-status--error states
  • .form-honeypot off-screen hide (position absolute, opacity 0, pointer-events none)

README.md

  • Step-by-step setup: create sheet → paste script → deploy as Web App → drop URL into SCRIPT_URL in form-handler.js and action in index.html
  • Deployment access recommendation changed to "Anyone with the link"
  • Anti-abuse measures table documenting each protection built into the Apps Script

Activation

Two places to update after deploying Code.gs as a Web App (Execute as: Me / Anyone with the link):

// form-handler.js
var SCRIPT_URL = 'https://script.google.com/macros/s/<DEPLOYMENT_ID>/exec';
<!-- index.html <form> tag -->
<form ... action="https://script.google.com/macros/s/<DEPLOYMENT_ID>/exec">

Preview

Original prompt

I'd like for the form on the front page of this site to connect to a Google Sheet to serve as lead generation and address collection.

Copilot AI changed the title [WIP] Add front page form to connect with Google Sheet for leads Connect contact form to Google Sheets via Apps Script Mar 31, 2026
Copilot AI requested a review from clearlineKris March 31, 2026 01:06
@clearlineKris clearlineKris marked this pull request as ready for review March 31, 2026 01:14
Copilot AI review requested due to automatic review settings March 31, 2026 01:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Replaces the existing Formspree contact form submission with a Google Apps Script Web App endpoint that appends lead + address details into a managed Google Sheet.

Changes:

  • Updates index.html contact form markup (new lead/address fields, status message element) and loads a new JS handler.
  • Adds form-handler.js to intercept submit and POST URL-encoded form data + timestamp to Apps Script.
  • Adds google-apps-script/Code.gs to create/maintain a Leads tab and append each submission as a new row; updates styling and setup docs accordingly.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
styles.css Widened form and added layout/status styles for new fields and submission feedback.
README.md Documents one-time deployment/setup steps for the Apps Script + front-end wiring.
index.html Replaces Formspree wiring with JS hook, adds new lead/address fields, and adds a status area.
google-apps-script/Code.gs Implements the Apps Script doPost handler that creates/appends to the Leads sheet.
form-handler.js Implements fetch-based form submission to Apps Script with optimistic success UX.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +13 to +15
// ── Replace with your deployed Google Apps Script Web App URL ────────────
var SCRIPT_URL = 'YOUR_GOOGLE_APPS_SCRIPT_URL';
// ─────────────────────────────────────────────────────────────────────────
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SCRIPT_URL is left as the placeholder string; as merged, submissions will always fail (and only show a generic error) until someone edits and re-deploys the site. Add a guard that detects an unconfigured/invalid SCRIPT_URL and shows an explicit “form not configured” message (and skips fetch), or load the URL from a config/data attribute so production builds aren’t committed in a broken state.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +50
var p = e.parameter;
sheet.appendRow([
// timestamp is always provided by form-handler.js; the fallback here
// guards against future clients that may not include it
p.timestamp || new Date().toISOString(),
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User-submitted fields are written into the sheet without sanitization, which enables spreadsheet formula injection (values starting with '=', '+', '-', or '@'). Sanitize/escape strings before appendRow so all submissions are stored as plain text.

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +27
/**
* doPost — called each time the contact form submits.
* Parameters are read from e.parameter (URL-encoded form body).
*/
function doPost(e) {
try {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName(SHEET_NAME);
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Web App endpoint accepts writes from anyone and appends directly to the Leads sheet, which makes it easy to spam/poison the spreadsheet. Add basic abuse controls (e.g., a honeypot field, rate limiting via CacheService, and/or rejecting requests that don’t include expected fields) so the sheet can’t be filled by arbitrary third parties.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you facilitate this for me @copilot? I'm not well versed with this kind of risk.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — commit 6730bb7 adds the following protections directly into Code.gs:

Control What it does
Honeypot A hidden hp field is added to the form (invisible to real users via off-screen CSS). Bots that blindly fill every field get silently discarded server-side.
Required-field guard Submissions missing name or email are rejected before anything is written to the sheet.
Rate limiting Each email address (normalized to collapse + aliases) is capped at 5 submissions per hour using CacheService. Requests over the cap get an error response without touching the sheet.
Formula injection protection sanitizeCell() prefixes any value starting with =, +, -, @, |, or % with a tab character so Google Sheets stores it as plain text instead of executing it as a formula.
Concurrent write safety LockService.getScriptLock() serializes simultaneous POSTs so sheet creation and row appends are race-condition-free. If the lock times out, the user gets a friendly "try again" message.

The README also has a full breakdown of each measure under "Anti-abuse measures built into the Apps Script".

Comment on lines +29 to +33
// Create the sheet and add headers if it doesn't exist yet
if (!sheet) {
sheet = ss.insertSheet(SHEET_NAME);
sheet.appendRow([
'Timestamp',
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doPost can be invoked concurrently; without a LockService lock around sheet creation/appendRow, simultaneous submissions can intermittently fail with spreadsheet service errors or create race conditions on first-run sheet creation. Consider using LockService.getScriptLock() to serialize writes (especially around insertSheet/appendRow).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to create a spreadsheet to output entries onto @copilot?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — the setup is already fully automated after a one-time manual step. Here's how it works:

  1. You create the Google Sheet (one time, takes 30 seconds) — just go to sheets.google.com, click +, and name it "ClearLine Leads".
  2. Paste Code.gs into that sheet's Apps Script editor and deploy it as a Web App (step-by-step instructions are in the README under "One-time setup").
  3. That's it — on the very first form submission, the script automatically creates a Leads tab with the correct column headers (Timestamp, Name, Email, Company, Address, City, State, Zip, Message) and freezes the header row. Every subsequent submission appends a new row. You never need to touch the spreadsheet structure manually.

The README's "What gets recorded" table shows exactly which form field maps to which column.

form-handler.js Outdated
Comment on lines +39 to +40
// The response is opaque — success is optimistic (the data still reaches
// the sheet; we cannot distinguish server-side errors from success).
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline comment says “the data still reaches the sheet” under no-cors, but that isn’t guaranteed—server-side errors can occur and fetch will still resolve with an opaque response. Reword to avoid implying delivery is assured when you can’t observe the HTTP status/body.

Suggested change
// The response is opaque — success is optimistic (the data still reaches
// the sheet; we cannot distinguish server-side errors from success).
// The response is opaque — success is optimistic (the request is sent,
// but we cannot verify delivery or distinguish server-side errors from success).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mt inexperience is showing @copilot

index.html Outdated
<textarea id="message" name="message" rows="4"></textarea>
</div>
<button type="submit" class="btn btn-primary btn-full">Send Message</button>
<div id="form-status" class="form-status" hidden></div>
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The status message container is visually updated but not announced to assistive technologies. Add accessibility attributes (e.g., role="status" and aria-live="polite" / aria-atomic="true") to #form-status so screen reader users get submission feedback.

Suggested change
<div id="form-status" class="form-status" hidden></div>
<div id="form-status" class="form-status" role="status" aria-live="polite" aria-atomic="true" hidden></div>

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm honestly lost as I'm doing this from a hospital bed @copilot

README.md Outdated
- Click **Deploy → New deployment**.
- Under *Select type*, choose **Web app**.
- Set **Execute as** → *Me*.
- Set **Who has access** → *Anyone*.
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setup instructs deploying the Web App with access set to “Anyone”, which makes the endpoint publicly writable and susceptible to spam. Consider documenting a minimal anti-abuse measure (honeypot/rate-limit) or recommending “Anyone with the link” / another restriction if feasible for the use case.

Suggested change
- Set **Who has access***Anyone*.
- Set **Who has access***Anyone with the link* (recommended).
- Using *Anyone with the link* reduces the chance of spam and automated abuse. If you must choose *Anyone*, add basic anti-abuse measures (e.g. a hidden honeypot field in the form and simple server-side validation/rate limiting in your Apps Script).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please assist me with protecting this form thusly @copilot

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already taken care of in commit 6730bb7. The form and Apps Script now have five layers of protection baked in — all documented in the README under "Anti-abuse measures built into the Apps Script":

  • Honeypot: hidden hp field catches bots that fill every input
  • Required-field guard: rejects submissions missing name or email
  • Rate limiting: 5 submissions per email per hour via CacheService
  • Formula injection protection: values that start with =, +, etc. are neutralized before being written to the Sheet
  • Concurrent write safety: LockService prevents race conditions

No extra work needed on your end — it's all live when you deploy Code.gs.

index.html Outdated
<p class="section-intro">Whether it's a question about compliance, a project idea, or just connecting — I'd like to hear from you.</p>

<form class="contact-form" action="https://formspree.io/f/maqnryyz" method="POST">
<form id="contact-form" class="contact-form">
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the form’s action/method makes submission depend entirely on JS; if the script fails to load (or JS is disabled), the form will POST/GET back to the same page and nothing reaches the sheet. Consider keeping a real action="…/exec" + method="POST" as a progressive-enhancement fallback (JS can still intercept to keep the in-page UX).

Suggested change
<form id="contact-form" class="contact-form">
<form id="contact-form" class="contact-form" method="post" action="https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec">

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please help @copilot

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already handled in commit 6730bb7. The <form> tag now has method="post" action="YOUR_GOOGLE_APPS_SCRIPT_URL" — once you drop your deployed Web App URL in there, the form will work even if JavaScript is disabled or fails to load. The JS handler still intercepts when it's available for the better in-page experience.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants