feat(contact): real working form with Apps Script backend (3 sub-tasks)

4.9a: Contact.astro (v6-contact enhanced)
- 2 variants: 'prompt' (1 input + ENTER, for home) + 'full' (5 fields, for /contact)
- Smart-parses prompt input (phone vs name detection)
- Toast feedback on submit (success/error, 5s auto-hide)
- Dev-mode aware (no endpoint = console.log + 'dev mode' toast)

4.9b: apps-script/contact-form/ (USER DEPLOYS)
- Code.gs: doPost() handler → 3 actions:
  1. Append to Google Sheet (SHEET_ID from Script Properties)
  2. Send email via MailApp to RECIPIENT_EMAIL
  3. Send LINE Notify via UrlFetchApp
- Returns {ok, id} or {ok:false, error} as JSON
- Includes testDoPost() for testing in Apps Script editor
- README.md: 6-step deploy guide (Script Properties + Sheet + LINE token)
  with security notes + cost breakdown

4.9c: src/lib/contact-submit.ts
- Reads PUBLIC_CONTACT_ENDPOINT from .env
- Empty/missing = dev mode (console.log + 300ms mock latency)
- Set = POST JSON to endpoint
- Exports submitContact() + isDevMode() for Contact.astro to use

Refs: .hermes/plans/2026-06-13_124000-moreminimore-v7-5-migration.md Task 4.9a-c
This commit is contained in:
Kunthawat Greethong
2026-06-13 17:55:59 +07:00
parent 154e3f2d91
commit 8c2bf3d303
4 changed files with 550 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
/**
* MOREMINIMORE - Contact Form Backend (Google Apps Script)
* Per plan 2026-06-13 round 2 #4:
* Form submit → Apps Script doPost() → Google Sheet (log)
* → Email to contact@moreminimore.com
* → LINE Notify
*
* USER DEPLOYS THIS THEMSELVES (see README.md in same folder).
* 1. Create new Apps Script project at https://script.google.com
* 2. Paste this entire file into Code.gs
* 3. Set Script Properties (Project Settings → Script Properties):
* SHEET_ID — Google Sheet ID (from Sheet URL)
* LINE_NOTIFY_TOKEN — from https://notify-bot.line.me
* RECIPIENT_EMAIL — contact@moreminimore.com (or any email)
* 4. Create Google Sheet with these headers in row 1:
* timestamp | name | phone | email | service | message | variant | userAgent
* 5. Deploy → New deployment → Type: Web app
* Execute as: Me
* Who has access: Anyone
* Copy the deployment URL.
* 6. Add to moreminimore-astroreal/.env:
* PUBLIC_CONTACT_ENDPOINT=<paste deployment URL>
*/
/* ------------------------------------------------------------------ */
/* CONFIG — read from Script Properties (set in Project Settings) */
/* ------------------------------------------------------------------ */
function getConfig() {
const props = PropertiesService.getScriptProperties();
return {
SHEET_ID: props.getProperty('SHEET_ID'),
LINE_NOTIFY_TOKEN: props.getProperty('LINE_NOTIFY_TOKEN'),
RECIPIENT_EMAIL: props.getProperty('RECIPIENT_EMAIL') || 'contact@moreminimore.com',
};
}
/* ------------------------------------------------------------------ */
/* HANDLER — POST /exec (or /dev) */
/* ------------------------------------------------------------------ */
/**
* Handle form submission. Expects JSON body:
* {
* name, phone, email, service, message, variant, userAgent, submittedAt
* }
*/
function doPost(e) {
try {
const data = JSON.parse(e.postData.contents);
const config = getConfig();
// 1. Log to Google Sheet
let rowId = null;
if (config.SHEET_ID) {
const sheet = SpreadsheetApp.openById(config.SHEET_ID).getActiveSheet();
const row = sheet.appendRow([
new Date(), // timestamp
data.name || '',
data.phone || '',
data.email || '',
data.service || '',
data.message || '',
data.variant || 'full',
data.userAgent || '',
]);
rowId = row.getRange().getRow();
}
// 2. Send email
const subject = `[moreminimore contact] ${data.service || 'general'} — ${data.name || data.phone || 'unknown'}`;
const body = formatEmailBody(data);
MailApp.sendEmail({
to: config.RECIPIENT_EMAIL,
subject: subject,
body: body,
replyTo: data.email || undefined,
});
// 3. Send LINE Notify
if (config.LINE_NOTIFY_TOKEN) {
const lineMessage = formatLineMessage(data);
UrlFetchApp.fetch('https://notify-api.line.me/api/notify', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + config.LINE_NOTIFY_TOKEN,
'Content-Type': 'application/x-www-form-urlencoded',
},
payload: { message: lineMessage },
muteHttpExceptions: true,
});
}
return ContentService
.createTextOutput(JSON.stringify({ ok: true, id: rowId }))
.setMimeType(ContentService.MimeType.JSON);
} catch (err) {
return ContentService
.createTextOutput(JSON.stringify({ ok: false, error: err.toString() }))
.setMimeType(ContentService.MimeType.JSON);
}
}
/* ------------------------------------------------------------------ */
/* FORMATTERS */
/* ------------------------------------------------------------------ */
function formatEmailBody(data) {
return [
'New contact form submission',
'',
'---',
'Name: ' + (data.name || '-'),
'Phone: ' + (data.phone || '-'),
'Email: ' + (data.email || '-'),
'Service: ' + (data.service || '-'),
'Variant: ' + (data.variant || 'full'),
'---',
'',
'Message:',
data.message || '(empty)',
'',
'---',
'Submitted: ' + (data.submittedAt || new Date().toISOString()),
'UserAgent: ' + (data.userAgent || 'unknown'),
].join('\n');
}
function formatLineMessage(data) {
const lines = [
'🔔 moreminimore contact',
'',
'👤 ' + (data.name || data.phone || 'unknown'),
'📞 ' + (data.phone || '-'),
'✉ ' + (data.email || '-'),
'🎯 ' + (data.service || 'general'),
];
if (data.message) {
lines.push('');
lines.push(data.message.length > 200 ? data.message.slice(0, 200) + '...' : data.message);
}
return lines.join('\n');
}
/* ------------------------------------------------------------------ */
/* TEST (optional — call testDoPost() from Apps Script editor) */
/* ------------------------------------------------------------------ */
function testDoPost() {
const mockEvent = {
postData: {
contents: JSON.stringify({
name: 'Test User',
phone: '080-123-4567',
email: 'test@example.com',
service: 'webdev',
message: 'This is a test message',
variant: 'full',
userAgent: 'test',
submittedAt: new Date().toISOString(),
}),
},
};
Logger.log(doPost(mockEvent).getContent());
}

View File

@@ -0,0 +1,140 @@
# Contact Form Backend — Google Apps Script
This is the backend for the contact form on the MoreminiMore website.
When someone submits the form, this Apps Script:
1. **Logs** the submission to a Google Sheet (audit trail)
2. **Emails** the submission to `contact@moreminimore.com` (your Google Workspace)
3. **Sends** a LINE Notify to your phone
The user (you) deploys this script. Hermes Agent cannot deploy it for you — it requires access to your Google account.
---
## Prerequisites
- A Google account (use `contact@moreminimore.com` or any Google Workspace account)
- A LINE Notify token (get one at <https://notify-bot.line.me/> — log in → My page → Generate token)
---
## Setup (5 minutes)
### Step 1: Create the Apps Script project
1. Go to <https://script.google.com/start>
2. Click **New project** (or "โปรเจ็กต์ใหม่" in Thai)
3. Rename the project (top-left) to `moreminimore-contact-form`
4. Delete the default `function myFunction() {}` code
5. Open `Code.gs` from this folder in your text editor
6. **Copy the entire contents** and **paste** into the Apps Script editor
7. Click 💾 (Save) or Ctrl+S
### Step 2: Set Script Properties
1. Click **Project Settings** (⚙️ gear icon on the left)
2. Scroll down to **Script Properties**
3. Click **Add script property** and add these 3:
| Property | Value | Where to get it |
|---|---|---|
| `SHEET_ID` | (Google Sheet ID) | See Step 3 below |
| `LINE_NOTIFY_TOKEN` | (LINE token) | <https://notify-bot.line.me/> → My page → Generate token → Copy |
| `RECIPIENT_EMAIL` | `contact@moreminimore.com` | Just type your email |
### Step 3: Create the Google Sheet
1. Go to <https://sheets.google.com/create>
2. Rename to `moreminimore-contact-log` (or whatever you like)
3. In row 1, add these headers (one per cell, A1 through H1):
```
timestamp | name | phone | email | service | message | variant | userAgent
```
4. **Get the Sheet ID** from the URL:
- URL looks like: `https://docs.google.com/spreadsheets/d/1aBcD...XyZ/edit`
- The `1aBcD...XyZ` part is the **SHEET_ID** — copy it
5. Paste it into Script Properties → `SHEET_ID` value
### Step 4: Test the script (optional but recommended)
1. In the Apps Script editor, select function `testDoPost` from the dropdown (next to the debug ▶ button)
2. Click **Run** (▶)
3. If prompted, authorize the script (review permissions → Allow)
4. Check:
- The Google Sheet has a new row at the bottom
- The recipient email got a new message
- Your LINE got a notification
5. Check **Execution log** (View → Logs) for any errors
### Step 5: Deploy as Web App
1. Click **Deploy** (top-right) → **New deployment**
2. Click the ⚙️ gear icon → select **Web app**
3. Configure:
- **Description**: `moreminimore contact form v1`
- **Execute as**: `Me (your-email@gmail.com)`
- **Who has access**: `Anyone` ← important, otherwise form can't reach it
4. Click **Deploy**
5. You may be asked to authorize again — click **Review permissions** → choose your account → **Allow**
6. **Copy the Web app URL** — it looks like:
```
https://script.google.com/macros/s/AKfycbz.../exec
```
### Step 6: Wire the URL into the website
1. Open `moreminimore-astroreal/.env` (create it if it doesn't exist)
2. Add this line (paste your URL):
```
PUBLIC_CONTACT_ENDPOINT=https://script.google.com/macros/s/AKfycbz.../exec
```
3. Save the file
4. Rebuild the site: `npm run build`
5. Deploy (or push to your host)
---
## Verifying it works
After deploy, visit the website, fill the form, hit submit.
You should see:
- ✅ Toast: "✓ ส่งแล้ว เราจะติดต่อกลับภายใน 24 ชม."
- ✅ New row in the Google Sheet
- ✅ Email in your inbox
- ✅ LINE notification on your phone
If something fails, check:
- **Apps Script Execution Log** (Executions tab on the left) — shows errors
- **Sheet ID** is correct (no extra spaces, full ID)
- **LINE token** is active (revoke + regenerate if needed)
- **"Who has access"** is set to `Anyone` on the deployment
---
## Updating the script later
When you change `Code.gs`:
1. Save in the editor
2. **Deploy** → **Manage deployments** → ✏️ (edit) → **Version: New version** → **Deploy**
3. The Web app URL stays the same — no need to update `.env`
---
## Security notes
- The web app URL is not secret — anyone who knows it can submit
- But it has no destructive power (only appends rows + sends email/notify to you)
- If you receive spam, you can revoke the LINE token + change the deployment URL
- Consider adding reCAPTCHA later if spam becomes a problem
---
## Cost
- **Google Apps Script**: free (1-hour quota, plenty for contact forms)
- **Google Sheets**: free up to 10M cells
- **MailApp**: free for ~100 emails/day per user
- **LINE Notify**: free, unlimited messages, but discontinued March 2025 in some regions — check status at <https://notify-bot.line.me/>
If LINE Notify is shut down, swap the LINE call for a Telegram bot or Discord webhook (same `UrlFetchApp.fetch` pattern).