<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Selfloom</title>
    <description>The latest articles on DEV Community by Selfloom (@ttcd77).</description>
    <link>https://dev.to/ttcd77</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3957253%2F811a92e5-64c2-4815-aadd-a4fd27cad56d.png</url>
      <title>DEV Community: Selfloom</title>
      <link>https://dev.to/ttcd77</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ttcd77"/>
    <language>en</language>
    <item>
      <title>Stop configuring Claude Code manually — I built a free generator</title>
      <dc:creator>Selfloom</dc:creator>
      <pubDate>Fri, 29 May 2026 16:47:51 +0000</pubDate>
      <link>https://dev.to/ttcd77/stop-configuring-claude-code-manually-i-built-a-free-generator-4201</link>
      <guid>https://dev.to/ttcd77/stop-configuring-claude-code-manually-i-built-a-free-generator-4201</guid>
      <description>&lt;p&gt;I use Claude Code every day. It is powerful, but every new project starts with the same small chore: setting up &lt;code&gt;.claude&lt;/code&gt; files, remembering useful slash commands, and writing enough project context so the agent does not start from zero.&lt;/p&gt;

&lt;p&gt;That setup work is not hard. It is just repetitive. On a real project it can easily burn 20-30 minutes before the useful work begins.&lt;/p&gt;

&lt;p&gt;So I built a small browser-only tool that generates a starter Claude Code setup.&lt;/p&gt;

&lt;p&gt;Try it here: &lt;a href="http://88.208.213.99/claude-code-config-builder/" rel="noopener noreferrer"&gt;http://88.208.213.99/claude-code-config-builder/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it generates
&lt;/h2&gt;

&lt;p&gt;The tool asks for a few project details and produces three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;settings.json&lt;/code&gt; for basic Claude Code preferences&lt;/li&gt;
&lt;li&gt;slash command templates for common workflows&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;CLAUDE.md&lt;/code&gt; starter file so the agent has project context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to replace thoughtful project setup. The goal is to remove the blank-page work and give you a clean first draft.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I made it browser-only
&lt;/h2&gt;

&lt;p&gt;I did not want a signup flow, backend, or API key requirement for something this simple.&lt;/p&gt;

&lt;p&gt;The current version runs in the browser. You fill in a few fields, generate the files, review them, and copy them into your project.&lt;/p&gt;

&lt;p&gt;No project data needs to leave your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example output
&lt;/h2&gt;

&lt;p&gt;A generated &lt;code&gt;CLAUDE.md&lt;/code&gt; starts with the basics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Project: Your Project Name&lt;/span&gt;

&lt;span class="gu"&gt;## Overview&lt;/span&gt;
One sentence describing what this project does.

&lt;span class="gu"&gt;## Conventions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Language: TypeScript / Python / etc.
&lt;span class="p"&gt;-&lt;/span&gt; Framework: Next.js / FastAPI / etc.
&lt;span class="p"&gt;-&lt;/span&gt; Testing: Jest / pytest / etc.

&lt;span class="gu"&gt;## Build &amp;amp; Run&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; npm run dev
&lt;span class="p"&gt;-&lt;/span&gt; npm test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generated slash commands are simple patterns like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;/review-pr
/generate-tests
/explain-file
/refactor
/summarize-changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They are intentionally boring. Boring is good here. These are the commands I find myself recreating across projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;This is mainly for people who:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use Claude Code across multiple projects&lt;/li&gt;
&lt;li&gt;want a quick starting structure&lt;/li&gt;
&lt;li&gt;do not want to copy a huge template repo&lt;/li&gt;
&lt;li&gt;still want to review the generated files before using them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you already have a mature internal agent setup, this may be too basic. If you are starting a new repo and want a clean baseline, it should save a few minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I want feedback on
&lt;/h2&gt;

&lt;p&gt;I am looking for feedback on three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Are the generated defaults useful?&lt;/li&gt;
&lt;li&gt;Which slash commands would you actually reuse?&lt;/li&gt;
&lt;li&gt;What should the tool generate next?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The tool is live here:&lt;/p&gt;

&lt;p&gt;&lt;a href="http://88.208.213.99/claude-code-config-builder/" rel="noopener noreferrer"&gt;http://88.208.213.99/claude-code-config-builder/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: this is my project. It is free right now, and there is a GitHub Sponsors link if it saves you time.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Build a Chrome extension that switches localhost/staging/production URLs</title>
      <dc:creator>Selfloom</dc:creator>
      <pubDate>Thu, 28 May 2026 19:58:49 +0000</pubDate>
      <link>https://dev.to/ttcd77/build-a-chrome-extension-that-switches-localhoststagingproduction-urls-28hc</link>
      <guid>https://dev.to/ttcd77/build-a-chrome-extension-that-switches-localhoststagingproduction-urls-28hc</guid>
      <description>&lt;p&gt;Switching between localhost, staging, and production should not be a manual URL-editing task.&lt;/p&gt;

&lt;p&gt;If you are testing web apps all day, you probably do this constantly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy a production or staging URL.&lt;/li&gt;
&lt;li&gt;Replace the hostname with localhost.&lt;/li&gt;
&lt;li&gt;Keep the path.&lt;/li&gt;
&lt;li&gt;Keep the query string.&lt;/li&gt;
&lt;li&gt;Keep the hash.&lt;/li&gt;
&lt;li&gt;Hope you did not typo anything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A tiny Chrome extension can remove that workflow completely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the extension does
&lt;/h2&gt;

&lt;p&gt;The extension reads the current tab URL, detects which environment you are on, and lets you jump to another configured environment while preserving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;path&lt;/li&gt;
&lt;li&gt;query string&lt;/li&gt;
&lt;li&gt;hash&lt;/li&gt;
&lt;li&gt;custom ports&lt;/li&gt;
&lt;li&gt;custom base paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://staging.example.com/products/42?tab=reviews#pricing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can become:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:3000/products/42?tab=reviews#pricing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;in one click.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manifest V3 structure
&lt;/h2&gt;

&lt;p&gt;The extension is deliberately small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;url-switcher/
??? manifest.json
??? popup.html
??? popup.js
??? popup.css
??? options.html
??? options.js
??? background.js
??? icons/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The manifest only needs two permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manifest_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"URL Environment Switcher"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"storage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tabs"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"default_popup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"popup.html"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"options_page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"options.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"background"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"service_worker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"background.js"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tabs&lt;/code&gt; is needed to read and update the active tab URL. &lt;code&gt;storage&lt;/code&gt; is needed to save environment settings.&lt;/p&gt;

&lt;p&gt;No host permissions are required because the extension does not inspect page content.&lt;/p&gt;

&lt;h2&gt;
  
  
  The switching algorithm
&lt;/h2&gt;

&lt;p&gt;The core logic is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the current tab URL.&lt;/li&gt;
&lt;li&gt;Compare its origin with configured environment origins.&lt;/li&gt;
&lt;li&gt;Find the active environment.&lt;/li&gt;
&lt;li&gt;Strip the active environment base path.&lt;/li&gt;
&lt;li&gt;Build the target URL with the target origin plus the original path/query/hash.&lt;/li&gt;
&lt;li&gt;Navigate the tab.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important detail is using &lt;code&gt;URL.origin&lt;/code&gt; instead of string prefix matching.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;activeIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;envs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;envUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;envUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Origin matching avoids mistakes like treating &lt;code&gt;localhost:3000&lt;/code&gt; and &lt;code&gt;localhost:3001&lt;/code&gt; as the same environment.&lt;/p&gt;

&lt;p&gt;Then preserve the rest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetEnv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;remainingPath&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nextUrl&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Options page
&lt;/h2&gt;

&lt;p&gt;The options page lets developers define their own environments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Localhost: &lt;code&gt;http://localhost:3000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Staging: &lt;code&gt;https://staging.example.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Production: &lt;code&gt;https://example.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These settings are stored in &lt;code&gt;chrome.storage.sync&lt;/code&gt;, so they follow the user across Chrome profiles where sync is enabled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is useful
&lt;/h2&gt;

&lt;p&gt;This is not a complex extension. That is the point.&lt;/p&gt;

&lt;p&gt;It removes a tiny repeated annoyance from web development. If you switch environments dozens of times a day, those tiny cuts add up quickly.&lt;/p&gt;

&lt;p&gt;It is also a clean Manifest V3 learning project: popup UI, options page, storage, tab navigation, and service worker structure without frameworks or build tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source code
&lt;/h2&gt;

&lt;p&gt;The full source is MIT licensed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ttcd77/url-switcher" rel="noopener noreferrer"&gt;https://github.com/ttcd77/url-switcher&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can download the ZIP and load it unpacked in Chrome today. Chrome Web Store publishing is blocked for me until developer identity verification is available, so unpacked loading is the current zero-budget distribution path.&lt;/p&gt;

&lt;p&gt;Disclosure: this article was drafted with AI assistance and manually checked before posting.&lt;/p&gt;

</description>
      <category>chromeextension</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Build a $0/month contact form backend with Google Sheets</title>
      <dc:creator>Selfloom</dc:creator>
      <pubDate>Thu, 28 May 2026 19:58:13 +0000</pubDate>
      <link>https://dev.to/ttcd77/build-a-0month-contact-form-backend-with-google-sheets-27lg</link>
      <guid>https://dev.to/ttcd77/build-a-0month-contact-form-backend-with-google-sheets-27lg</guid>
      <description>&lt;p&gt;Static sites do not need a monthly form backend.&lt;/p&gt;

&lt;p&gt;If you build portfolio sites, landing pages, or simple client websites, the contact form is usually the awkward bit. You do not want to run a server just to collect a name, email, and message. But many hosted form services charge $10-20/month once you want Google Sheets, notifications, or no branding.&lt;/p&gt;

&lt;p&gt;A small Google Apps Script can do the same job for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The flow is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HTML form -&amp;gt; Google Apps Script doPost -&amp;gt; Google Sheet
                                      -&amp;gt; email notification
                                      -&amp;gt; optional auto-reply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your form sends a POST request to a Google Apps Script web app. The script validates the submission, filters obvious spam, writes a row to Google Sheets, and sends an email notification with &lt;code&gt;MailApp.sendEmail()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No server. No database. No SMTP provider. No monthly invoice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Core handler
&lt;/h2&gt;

&lt;p&gt;The endpoint is a &lt;code&gt;doPost&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;doPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;handlePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Internal server error.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;handlePost&lt;/code&gt;, the useful pipeline is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parse JSON or form-urlencoded input.&lt;/li&gt;
&lt;li&gt;Check a honeypot field.&lt;/li&gt;
&lt;li&gt;Validate required fields.&lt;/li&gt;
&lt;li&gt;Reject malformed email addresses.&lt;/li&gt;
&lt;li&gt;Apply simple keyword spam filtering.&lt;/li&gt;
&lt;li&gt;Rate limit repeated submissions.&lt;/li&gt;
&lt;li&gt;Append a row to Google Sheets.&lt;/li&gt;
&lt;li&gt;Send the owner an email notification.&lt;/li&gt;
&lt;li&gt;Optionally send an auto-reply.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A honeypot field is especially useful. Bots often fill every field they see. Humans never fill a hidden &lt;code&gt;_gotcha&lt;/code&gt; field. If that field has a value, return a fake success and do not write to the sheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google Sheets as the database
&lt;/h2&gt;

&lt;p&gt;For a contact form, Google Sheets is a perfectly reasonable database. It is searchable, exportable, and easy for non-technical clients to understand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ensureSheet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SpreadsheetApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getActiveSpreadsheet&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;sheet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSheetByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;sheet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insertSheet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendRow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Timestamp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Source URL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFrozenRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;sheet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;D:D&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setWrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then append the submission:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;appendRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;SpreadsheetApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getActiveSpreadsheet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSheetByName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sheetName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Email notifications without SMTP
&lt;/h2&gt;

&lt;p&gt;Apps Script includes &lt;code&gt;MailApp.sendEmail()&lt;/code&gt;, so the script can notify the site owner without SendGrid, Mailgun, or SMTP setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;MailApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipientEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;New Contact Form Submission&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt; sent:\n\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is enough for many client sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTML integration
&lt;/h2&gt;

&lt;p&gt;Your frontend only needs a normal submit handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SCRIPT_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;_gotcha&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_gotcha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy the Apps Script as a web app, set access to "Anyone", copy the deployment URL, and paste it into your form JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  When this is a good fit
&lt;/h2&gt;

&lt;p&gt;This pattern works well for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;portfolio contact forms&lt;/li&gt;
&lt;li&gt;agency landing pages&lt;/li&gt;
&lt;li&gt;small business websites&lt;/li&gt;
&lt;li&gt;waitlists&lt;/li&gt;
&lt;li&gt;internal request forms&lt;/li&gt;
&lt;li&gt;low-volume lead capture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not the right tool for file uploads, payment forms, or very high-volume transactional workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source code
&lt;/h2&gt;

&lt;p&gt;I published a complete MIT-licensed implementation here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ttcd77/form-handler" rel="noopener noreferrer"&gt;https://github.com/ttcd77/form-handler&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It includes Google Sheets storage, email notifications, honeypot spam protection, keyword filtering, rate limiting, auto-replies, and an example HTML form.&lt;/p&gt;

&lt;p&gt;The project also has a one-time paid version for teams that want setup UI and multi-form quality-of-life features, but the open-source script is fully usable on its own.&lt;/p&gt;

&lt;p&gt;Disclosure: this article was drafted with AI assistance and manually checked before posting.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>serverless</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
