Static site for Molly Loch.
The contact page posts to a Google Apps Script URL stored in secrets.js, which is gitignored so it is not committed to the repository.
Local setup: copy the example file and add your script URL:
cp secrets.example.js secrets.jsEdit secrets.js and set:
contactFormScriptUrl— your deployed Google Apps Script web app URLrecaptchaSiteKey— your reCAPTCHA v2 site key (public; used by the contact page widget)
The contact form will not work until this file exists. reCAPTCHA also requires the secret key in Apps Script (see below); that key must never go in secrets.js.
Deploy: copy secrets.js to the server alongside the site files (it is not included in git).
The contact page uses reCAPTCHA v2 (“I'm not a robot” checkbox) to reduce spam.
Use the classic admin console — it shows both keys on one screen and works with this site’s Apps Script verification.
- Open Create a reCAPTCHA key (classic admin).
- Choose Challenge (v2) → “I'm not a robot” Checkbox.
- Add your domains (e.g.
localhostfor local testing and your production domain). - Submit — Google shows a Site key and Secret key immediately.
- Copy the Site key into
secrets.jsasrecaptchaSiteKey. - Copy the Secret key into Apps Script properties (step 2 below).
If you created the key in Google Cloud Console instead: the UI often shows “Incomplete” and may not display a secret key clearly. You can either hunt for Integration → Legacy reCAPTCHA secret key (the “Use legacy key” link is missing in some views), or create a fresh key via the classic admin link above and use that pair instead.
In your Apps Script project, open Project settings → Script properties and add:
| Property | Value |
|---|---|
RECAPTCHA_SECRET_KEY |
Your reCAPTCHA secret key |
The contact form sends name, email, message, and g-recaptcha-response via POST to a Google Apps Script web app, which verifies reCAPTCHA and appends each submission to a Google Sheet.
-
In Google Sheets, create a new spreadsheet (e.g. “Contact form submissions”).
-
In row 1, add these column headers:
Timestamp Name Email Message -
Leave the sheet named Sheet1 (or change
SHEET_NAMEin the script below to match).
-
In the spreadsheet, open Extensions → Apps Script.
-
Copy your Spreadsheet ID from the sheet URL:
https://docs.google.com/spreadsheets/d/THIS_PART_IS_THE_ID/edit -
Delete any default code and paste (replace
YOUR_SPREADSHEET_ID):
const SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID';
const SHEET_NAME = 'Sheet1';
function getSheet() {
return SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);
}
function doPost(e) {
try {
const sheet = getSheet();
const params = e.parameter;
const recaptchaToken = (params['g-recaptcha-response'] || '').trim();
const recaptcha = verifyRecaptcha(recaptchaToken);
if (!recaptcha.ok) {
return jsonResponse({ status: 'error', message: recaptcha.message });
}
const name = (params.name || '').trim();
const email = (params.email || '').trim();
const message = (params.message || '').trim();
if (!name || !email || !message) {
return jsonResponse({ status: 'error', message: 'Missing required fields.' });
}
sheet.appendRow([new Date(), name, email, message]);
return jsonResponse({ status: 'success' });
} catch (err) {
return jsonResponse({ status: 'error', message: err.message });
}
}
function verifyRecaptcha(token) {
if (!token) {
return { ok: false, message: 'Missing reCAPTCHA token.' };
}
const secret = PropertiesService.getScriptProperties().getProperty('RECAPTCHA_SECRET_KEY');
if (!secret) {
throw new Error('RECAPTCHA_SECRET_KEY is not set in Script properties.');
}
const response = UrlFetchApp.fetch('https://www.google.com/recaptcha/api/siteverify', {
method: 'post',
payload: {
secret: secret,
response: token
}
});
const result = JSON.parse(response.getContentText());
if (result.success === true) {
return { ok: true };
}
const codes = (result['error-codes'] || []).join(', ') || 'unknown';
return { ok: false, message: 'reCAPTCHA verification failed (' + codes + ').' };
}
function jsonResponse(obj) {
return ContentService
.createTextOutput(JSON.stringify(obj))
.setMimeType(ContentService.MimeType.JSON);
}- Click Save, then name the project (e.g. “Contact form handler”).
Why
openById? When the script runs as a public web app,getActiveSpreadsheet()returnsnull. Always use the spreadsheet ID instead.
- Click Deploy → New deployment.
- Select type Web app.
- Set Execute as to Me and Who has access to Anyone (required so the public website can POST to it).
- Click Deploy, authorize when prompted, and copy the Web app URL (it ends with
/exec).
- Copy
secrets.example.jstosecrets.jsif you have not already. - Set
contactFormScriptUrlandrecaptchaSiteKeyinsecrets.js. - Add
RECAPTCHA_SECRET_KEYto Apps Script properties (see reCAPTCHA setup above). - Open the contact page, complete the reCAPTCHA checkbox, submit a test message, and confirm a new row appears in the sheet.
If submissions fail, check Apps Script → Executions for errors. After you change the script, create a New deployment (or manage deployments and update the version) so the live URL picks up your changes.
That is normal for doPost web apps. The Result column is often empty even when the script ran successfully. Check these instead:
- Status — should be Completed (not Failed).
- Your spreadsheet — did a new row appear after submit?
- The contact page — what message appears under the form?
- Browser DevTools → Network — click the request to
script.google.com, open Response, and read the JSON (status: successor an errormessage).
To inspect server-side logging, add console.log(...) lines in doPost, submit the form again, then open the execution and click View logs (or Cloud logs).
Run this once from the Apps Script editor (Run → testSetup), then open View → Logs:
function testSetup() {
const secret = PropertiesService.getScriptProperties().getProperty('RECAPTCHA_SECRET_KEY');
console.log('RECAPTCHA_SECRET_KEY set:', secret ? 'yes' : 'NO — add it in Project settings → Script properties');
const sheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName(SHEET_NAME);
console.log('Sheet "' + SHEET_NAME + '" found:', sheet ? 'yes' : 'NO — check SPREADSHEET_ID and SHEET_NAME');
}| Symptom | Likely cause |
|---|---|
RECAPTCHA_SECRET_KEY is not set |
Add the secret key in Project settings → Script properties (exact name, all caps). |
reCAPTCHA verification failed |
Secret key does not match the site key in secrets.js, or domain not listed in reCAPTCHA admin. |
Unexpected response (not JSON) |
Wrong web app URL, or script was changed but not redeployed. Create a New deployment and update secrets.js. |
| No executions at all | Wrong URL in secrets.js, or form blocked before submit (reCAPTCHA not completed). |
| Execution Failed | Open it for the stack trace — often a missing sheet name or permission issue. |
Cannot read properties of null (reading 'getSheetByName') |
Replace getActiveSpreadsheet() with SpreadsheetApp.openById(SPREADSHEET_ID) — see script above. |
reCAPTCHA verification failed (invalid-input-secret) |
Secret key in Script properties does not match the site key in secrets.js. Paste the secret key from the same classic-admin registration. |
reCAPTCHA verification failed (invalid-input-response) |
Domain mismatch — add localhost and 127.0.0.1 in reCAPTCHA admin; serve the site over http://localhost, not file://. |
| Same error after new keys / new deploy URL | Script properties are per Apps Script project. A new deployment URL often means a new project — re-add RECAPTCHA_SECRET_KEY and confirm SPREADSHEET_ID is in the code. |
testSetup logs Secret set: NO after adding the property |
Wrong project, wrong properties tab, or wrong property name — see below. |
Script properties are saved per Apps Script project, not per Google account or spreadsheet file in general.
- Open the spreadsheet
1JXXISJrzNWqY_5AqaXLZVc1ikSzWNiQWiwZt5-x5OPA→ Extensions → Apps Script (this must be the same project whose URL is insecrets.js). - In Apps Script, click the gear icon (Project settings) in the left sidebar — not Google Sheets settings.
- Scroll to Script properties (not User properties).
- Add a row:
- Property:
RECAPTCHA_SECRET_KEY(exact spelling, all caps) - Value: your classic-admin Secret key (the long
6L...string)
- Property:
- Click Save script properties at the bottom of that page.
List what is actually saved — run this in the same project, then View → Logs:
function listProperties() {
const keys = Object.keys(PropertiesService.getScriptProperties().getProperties());
console.log('Script property names:', keys.length ? keys.join(', ') : '(none)');
}If you see (none) or a name other than RECAPTCHA_SECRET_KEY, the property was not saved in this project.
Set it from code once (if the UI is not sticking — run setSecretOnce once, then delete this function):
function setSecretOnce() {
PropertiesService.getScriptProperties().setProperty(
'RECAPTCHA_SECRET_KEY',
'PASTE_YOUR_CLASSIC_ADMIN_SECRET_KEY_HERE'
);
console.log('Done. Run testSetup to confirm.');
}Run setSecretOnce → testSetup. You should see Secret set: yes.