Build an embeddable feedback widget
Collect input from any Northwind page via a small JavaScript snippet that posts to your script.
Published Aug 24, 2025
The best feedback is the kind people leave in the moment — when they cannot find the button they wanted, when the wording confuses them, when something quietly delights. By the time they have closed the tab and filed it under “things I might email someone about later”, the moment is gone. Northwind needs a way to catch that input where it happens, on the page itself, without shipping a third-party widget that loads a megabyte of tracking with it.
This is a one-script, one-snippet feedback widget. The Apps Script side is a
doPost web app that appends submissions to a Sheet. The page side is a tiny
button you paste into any Northwind site — internal wiki, marketing page,
docs — that prompts for a message and posts it to your script. No backend, no
build step, no dependencies.
What you’ll need
- A Google Sheet to store submissions. The script appends one row per message with timestamp, source URL, the message itself, and an optional email.
- An Apps Script project containing the
doPostfunction below, deployed as a web app with access set to Anyone. - Editor access to any HTML page where the widget will live — the snippet is one button and a few lines of JavaScript.
The web-app handler
// The sheet that stores submitted feedback. Headers expected in row 1:
// timestamp | url | message | email
const FEEDBACK_SHEET_ID = '1abcFeedbackId';
// Cap on message length — anything past this gets truncated before storage.
// Stops a runaway paste from breaking the row.
const MAX_MESSAGE_LENGTH = 4000;
/**
* Receives a feedback POST from the embedded widget and appends it to
* the feedback sheet. Returns plain text so the browser sees a clean
* 200 response.
*
* Expected payload: { url, message, email? }
*/
function doPost(e) {
// 1. Guard against empty or malformed bodies — a misconfigured embed
// can fire a POST with no payload at all.
if (!e || !e.postData || !e.postData.contents) {
return ContentService.createTextOutput('Empty body');
}
let data;
try {
data = JSON.parse(e.postData.contents);
} catch (err) {
return ContentService.createTextOutput('Bad JSON');
}
// 2. The message is the only field we genuinely need. Reject blank ones
// so the sheet does not fill with empty rows from accidental submits.
const message = String(data.message || '').trim();
if (!message) {
return ContentService.createTextOutput('No message');
}
// 3. Append the row. The source URL helps you tell where each piece of
// feedback came from; email is optional.
SpreadsheetApp.openById(FEEDBACK_SHEET_ID).getSheets()[0].appendRow([
new Date(),
String(data.url || ''),
message.slice(0, MAX_MESSAGE_LENGTH),
String(data.email || ''),
]);
return ContentService.createTextOutput('OK');
}
The widget (paste on any page)
<!-- Drop this anywhere on a Northwind page. The button floats wherever
you place it in the markup — style it to match your design. -->
<button onclick="showFeedbackWidget()">Feedback</button>
<script>
// Replace this with the /exec URL of your deployed Apps Script web app.
const FEEDBACK_ENDPOINT = 'https://script.google.com/macros/s/AKfycb.../exec';
function showFeedbackWidget() {
// The simplest possible UI: a native prompt. Swap for a modal if you
// need styling, but this version has zero dependencies.
const msg = prompt('What can we improve?');
if (!msg) return;
// Fire-and-forget POST. We do not await the response because Apps
// Script web apps return a CORS-allowed text/plain body that some
// browsers refuse to read from a cross-origin fetch — so we just
// send and move on.
fetch(FEEDBACK_ENDPOINT, {
method: 'POST',
// text/plain avoids a CORS preflight, which Apps Script does not
// answer. The server still parses JSON from the body.
headers: { 'Content-Type': 'text/plain;charset=utf-8' },
body: JSON.stringify({
url: location.href,
message: msg,
}),
}).catch(() => {
// Network errors are silent on purpose — the user does not need to
// know whether the row landed in the sheet.
});
alert('Thanks — sent.');
}
</script>
How it works
- A visitor clicks the Feedback button. The script uses
prompt()to ask for a message — primitive, but it works on every browser and needs no CSS. - The page POSTs a JSON body with the current
location.hrefand the typed message to the web-app/execURL. doPostparses the body, guards against empty messages and broken JSON, and appends one row to the feedback sheet with a timestamp, the source URL, the message, and the optional email.- Messages are truncated to
MAX_MESSAGE_LENGTHso a stray paste of a 50 KB essay does not break the row. The browser sees a200 OKresponse either way and the user gets analertconfirming the send.
Example run
A reader on https://northwind.example/docs/setup clicks Feedback, types
Step 4 says click 'next' but the button is labelled 'continue' and hits OK.
The feedback sheet picks up a new row:
| timestamp | url | message | |
|---|---|---|---|
| 2025-08-24 14:12:09 | https://northwind.example/docs/setup | Step 4 says click ‘next’ but the button is labelled ‘continue’ |
A second submitter pastes a 12,000-character bug report — the row stores the first 4,000 characters; the rest is dropped on the floor.
Deploy it
- Paste the
doPostscript into a new Apps Script project. - Click Deploy → New deployment, pick Web app, set Execute as to your account and Who has access to Anyone (required because the widget POSTs from a third-party page, unauthenticated).
- Copy the
/execURL and paste it intoFEEDBACK_ENDPOINTin the widget snippet. - Drop the snippet into any Northwind page and submit a test row to confirm the sheet receives it.
When you edit the server code you must redeploy a new version — Deploy → Manage deployments → edit → New version — and the existing URL stays the same.
Watch out for
- The endpoint is public by design. Anyone who finds the URL can POST to it,
so do not expose any sensitive logic from
doPost. Add a shared secret in the payload (data.secret === 'xyz') if you need to dissuade casual abuse. fetchwithContent-Type: text/plainskips the CORS preflight that Apps Script does not answer. Setting it toapplication/jsonwill look fine in dev tools but the request never reaches your script.- The browser usually cannot read the response body cross-origin, so do not rely on it. Validate that submissions arrive by watching the sheet, not the network panel.
- High-volume pages will hit the daily web-app trigger quota. For a heavily trafficked site, batch submissions or write to a queue first — see Cache API responses to stay under quotas.
- A
prompt()is rough UX. Once the flow is proven, swap it for a real modal with a textarea and an email field — the server already accepts both.
Related
Build a branded approval interface
Approve Northwind requests through a custom UI — clients click, decision is logged.
Updated Nov 8, 2025
Build an interactive quiz or assessment app
Run Northwind tests with scoring and feedback — questions in a Sheet, results in another.
Updated Nov 4, 2025
Build a multi-page web app with routing
Structure a real Northwind app across views — query-param routing, shared layout.
Updated Oct 31, 2025
Build a form-to-PDF web service
Convert Northwind form submissions to PDFs on the fly — POST in, PDF out.
Updated Oct 27, 2025
Build an expiring secure-download generator
Issue time-limited Northwind links via a web app — token in URL, server-side check.
Updated Oct 23, 2025