-
Notifications
You must be signed in to change notification settings - Fork 0
Connect contact form to Google Sheets via Apps Script #43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,66 @@ | ||||||||||
| /** | ||||||||||
| * form-handler.js | ||||||||||
| * | ||||||||||
| * Submits the contact form to a Google Apps Script Web App which appends | ||||||||||
| * each lead to a Google Sheet. | ||||||||||
| * | ||||||||||
| * Setup: deploy google-apps-script/Code.gs as a Web App (see README for | ||||||||||
| * full instructions) and paste the resulting URL into SCRIPT_URL below. | ||||||||||
| */ | ||||||||||
| (function () { | ||||||||||
| 'use strict'; | ||||||||||
|
|
||||||||||
| // ── Replace with your deployed Google Apps Script Web App URL ──────────── | ||||||||||
| var SCRIPT_URL = 'YOUR_GOOGLE_APPS_SCRIPT_URL'; | ||||||||||
| // ───────────────────────────────────────────────────────────────────────── | ||||||||||
|
Comment on lines
+13
to
+15
|
||||||||||
|
|
||||||||||
| var form = document.getElementById('contact-form'); | ||||||||||
| if (!form) return; | ||||||||||
|
|
||||||||||
| var submitBtn = form.querySelector('button[type="submit"]'); | ||||||||||
| var statusEl = document.getElementById('form-status'); | ||||||||||
|
|
||||||||||
| form.addEventListener('submit', function (e) { | ||||||||||
| e.preventDefault(); | ||||||||||
|
|
||||||||||
| // Collect all form values | ||||||||||
| var params = new URLSearchParams(); | ||||||||||
| new FormData(form).forEach(function (value, key) { | ||||||||||
| params.append(key, value); | ||||||||||
| }); | ||||||||||
| params.append('timestamp', new Date().toISOString()); | ||||||||||
|
|
||||||||||
| // Disable button while in flight | ||||||||||
| submitBtn.disabled = true; | ||||||||||
| submitBtn.textContent = 'Sending\u2026'; | ||||||||||
|
|
||||||||||
| // POST to Google Apps Script | ||||||||||
| // no-cors makes this a simple CORS request so no preflight is needed. | ||||||||||
| // 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 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). |
There was a problem hiding this comment.
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
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,70 @@ | ||||||||||||||
| /** | ||||||||||||||
| * Code.gs — Google Apps Script for ClearLine Contact Form → Google Sheets | ||||||||||||||
| * | ||||||||||||||
| * Deployment instructions (see README for full details): | ||||||||||||||
| * 1. Open your target Google Sheet in Google Drive. | ||||||||||||||
| * 2. Click Extensions → Apps Script. | ||||||||||||||
| * 3. Replace any existing code with this file's contents. | ||||||||||||||
| * 4. Click Deploy → New deployment → Web app. | ||||||||||||||
| * - Execute as: Me | ||||||||||||||
| * - Who has access: Anyone | ||||||||||||||
| * 5. Click Deploy and copy the Web App URL. | ||||||||||||||
| * 6. Paste the URL into SCRIPT_URL in form-handler.js. | ||||||||||||||
| * | ||||||||||||||
| * The script automatically creates a "Leads" sheet tab (if absent) with | ||||||||||||||
| * the correct column headers on the first run. | ||||||||||||||
| */ | ||||||||||||||
|
|
||||||||||||||
| var SHEET_NAME = 'Leads'; | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * 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); | ||||||||||||||
|
||||||||||||||
| 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".
Outdated
Copilot
AI
Mar 31, 2026
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
- You create the Google Sheet (one time, takes 30 seconds) — just go to sheets.google.com, click +, and name it "ClearLine Leads".
- Paste
Code.gsinto 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"). - 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.
Outdated
Copilot
AI
Mar 31, 2026
There was a problem hiding this comment.
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.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -219,7 +219,7 @@ <h2>Stack & Tools</h2> | |||||
| <h2>Say Hello</h2> | ||||||
| <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"> | ||||||
|
||||||
| <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"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please help @copilot
There was a problem hiding this comment.
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.
Outdated
Copilot
AI
Mar 31, 2026
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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":hpfield catches bots that fill every inputCacheService=,+, etc. are neutralized before being written to the SheetLockServiceprevents race conditionsNo extra work needed on your end — it's all live when you deploy
Code.gs.