A WordPress plugin by Tech Matters that provides a contact/newsletter subscription form with Airtable integration.
Website: https://bd4d.org/ Version: 1.0.5 License: GPL-2.0-or-later
- Contact/newsletter subscription form (shortcode:
[bd4d-contact-form]) - Airtable integration for storing user data
- Google reCAPTCHA v3 for bot protection
- Auto-reply confirmation emails
- WordPress admin settings page for configuration
bd4d-plugin/ # Repo root (build tools & config)
├── Gruntfile.js # Build task configuration
├── package.json # NPM dependencies
├── composer.json # PHP dependencies
├── phpcs.xml # PHP CodeSniffer config
├── .github/ # CI/CD workflows (not deployed)
│ └── workflows/
│ ├── wpcs.yml # WordPress coding standards check
│ └── check-commits.yml # Conventional commits validation
│
└── wp-content/ # ← DEPLOYED TO PRESSABLE
└── plugins/
└── bd4d/ # The WordPress plugin
├── bd4d.php # Main plugin entry point
├── includes/
│ ├── class-bd4d.php # Core form & Airtable integration
│ ├── class-google-recaptcha.php # ReCAPTCHA verification
│ └── settings/
│ ├── class-settings.php # Base settings class
│ └── class-contact-form-settings.php # Admin settings
├── assets/
│ ├── css/src/main.scss # Styles (→ main.min.css)
│ └── js/src/main.js # Form JS (→ main.min.js)
└── template-parts/
├── form-email.php # Newsletter form HTML
└── auto-reply.php # Email template
- PHP with WordPress Plugin Architecture (requires WP 6.2+)
- JavaScript/jQuery for AJAX form handling
- SCSS for styling
- Grunt for build tasks (linting, compilation, minification)
- Composer for PHP dependencies
- Follows WordPress-VIP-Go coding standards
npm install && composer install # Install dependencies
npx grunt # Run all build tasks
npx grunt watch # Watch for changes
npx grunt css-js # Build CSS and JS only
npx grunt php # Run PHP linting/standardsSettings are managed in WordPress Admin under the BD4D settings page:
- Airtable Base ID, Table ID, API Token
- Google reCAPTCHA Site Key and Secret Key
The plugin writes form submissions to Airtable via the REST API (https://api.airtable.com/v0).
| Airtable Field | Form Input | Type | Required |
|---|---|---|---|
Email Address |
Email input | No | |
First Name |
First name input | Text | Yes |
Last Name |
Last name input | Text | Yes |
Affiliation |
Affiliation input | Text | No |
Form Comments |
Message textarea | Text | No |
Email-Opted In? |
Newsletter checkbox | Boolean | Always sent |
Endorser? |
Endorser checkbox | Boolean | Always sent |
Adoption? |
Adoption checkbox | Boolean | Always sent |
CotW-Opted In? |
Supporter checkbox — removed from form (v1.0.6); field retained for historical opt-ins | Boolean | No longer sent |
Note: First and Last Name are required. Email Address is required only when an opt-in checkbox (newsletter / endorser / adoption) is checked; otherwise it is optional.
class-bd4d.php- Containsadd()method that writes to Airtable (line ~153)form-email.php- Form HTML templateclass-contact-form-settings.php- Admin settings for API credentials
- Add the field/column directly in Airtable (no staging Airtable exists)
- Update
class-bd4d.php:- Add parameter to
add()method - Add field to
$data['fields']array - Update
send_message()to read from$_POSTand pass toadd()
- Add parameter to
- If field comes from form:
- Update
form-email.phpto add the HTML input - Update
assets/js/src/main.jsto read the input and include it in the AJAXdataobject
- Update
- Run
npx gruntto build assets - Commit (including built assets), push, deploy
- No staging Airtable environment - all environments write to the same Airtable base
- Plugin creates new records (POST), does not update existing ones
- Auto-reply email is sent after successful Airtable write
The form authenticates to Airtable with a Personal Access Token (PAT) stored in
WP Admin → BD4D → Contact Form Settings → Airtable Token. If that token is revoked,
expires, or loses access to the base, submissions fail with "Unable to send message"
and wp-content/debug.log shows a 403:
BD4D contact form Airtable API rejected submission: HTTP 403 body:
{"error":{"type":"INVALID_PERMISSIONS_OR_MODEL_NOT_FOUND", ...}}
To issue a replacement token:
- In Airtable, go to Builder Hub → Developers → Personal access tokens → Create new token.
- Name: something identifiable, e.g.
bd4d-airtable-YYYYMMDD. - Scopes: add both
data.records:readanddata.records:write. - Access: add the base BD4D-Relationships-Main XRM (base ID
appgrixUjq2JjgPbP). You can only grant access to a base your own account can open — confirm athttps://airtable.com/appgrixUjq2JjgPbP.
- Name: something identifiable, e.g.
- Click Create token and copy the
pat…value (shown only once). - In WP Admin → BD4D → Contact Form Settings, paste it into Airtable Token and save.
Leave Base ID (
appgrixUjq2JjgPbP) and Table ID (tbl9KA9XVlZW1FDau, the Individual Contacts-MAIN table) unchanged. - Submit the form to test. On success,
debug.logshowsBD4D contact form Airtable API responded HTTP 200.
Tip: create the token under a shared/service Airtable account rather than an individual's, so it does not break when a person loses access to the base.
The plugin sends a confirmation email after successful form submission. The email content varies based on which checkboxes were selected.
| Case | Newsletter | Supporter | Adoption |
|---|---|---|---|
| A | ✓ | ✗ | ✗ |
| B | ✗ | ✓ | ✗ |
| C | ✓ | ✓ | ✗ |
| D | any | any | ✓ |
| E | ✗ | ✗ | ✗ |
- Case A (Newsletter only): User subscribes to email updates. Gets unsubscribe instructions.
- Case B (Supporter only): User agrees to be listed as public supporter. Gets display permission confirmation.
- Case C (Newsletter + Supporter): User wants both. Gets combined confirmation with bullet points for both permissions plus unsubscribe instructions.
- Case D (Adoption): Takes priority. User wants to learn about adopting BD4D Standard. Gets personalized follow-up promise ("We will contact you personally within the next two business days").
- Case E (No checkboxes): User submits without selecting any options. Gets generic welcome message with no confirmation section.
auto-reply.php- Email template with conditional logic
| Environment | URL | Branch | Deploy Method |
|---|---|---|---|
| Production | bd4d.org | main |
Auto-deploy on push/merge to main (confirmed 2026-07-18) |
| Staging | bd4d-staging.mystagingwebsite.com | staging |
Auto-deploy on push to staging (configured 2026-07-19) |
| Sandbox | bd4dsandbox.mystagingwebsite.com | N/A | Static clone from staging (Oct 2025) |
Confirmed working 2026-07-18: merging a PR to main auto-deployed to production within
a couple of minutes with no manual "Set and Deploy" click. Only the bd4d plugin directory
was updated, and the deployed files were verified byte-identical to main.
These notes are not specific to this plugin and could still change based on Pressable-side configuration unrelated to this repo, so re-verify if a deploy ever behaves unexpectedly.
Each Pressable site (production and staging) has its own GitHub Integration pointing at this repo. A push/merge to a site's configured branch auto-deploys within a couple of minutes.
GitHub Pressable
────────────────── ─────────────────────────────────────────
main branch ─────► production (bd4d.org)
staging branch ─────► staging (bd4d-staging.mystagingwebsite.com)
wp-content/plugins/bd4d ─────► htdocs/wp-content/plugins/bd4d (both sites)
Each site's integration (Pressable dashboard → site → GitHub Integration) uses per-directory Include and Delete toggles. Verified configuration for both sites:
| Setting | Production | Staging |
|---|---|---|
| Branch | main |
staging |
| Include plugins directory | Yes | Yes |
| Include themes directory | No | No |
| Include MU Plugins directory | No | No |
| Delete plugin files not in repo | No | No |
| Delete theme files not in repo | No | No |
| Delete MU plugin files not in repo | No | No |
With "Include plugins: Yes" and the "Delete" toggles off, a deploy only adds/updates the
bd4d plugin and never removes or touches anything else. Note: selecting a branch in the dropdown
does nothing until you click Set and Deploy to commit it.
This repo contains only wp-content/plugins/bd4d. The server runs ~14 plugins (Divi Pixel,
Jetpack, Autoptimize, WordPress SEO, etc.) that are not in this repo. Enabling "Delete plugin
files not in repository" would delete all of them on the next deploy. Keep all three red
"Delete …" toggles off, permanently.
"Include MU Plugins" is off, so files under wp-content/mu-plugins/ must be copied to the server
manually (SFTP/rsync). Routine plugin deploys never touch them.
Server architecture:
htdocs/- Your site files (wp-content, wp-config.php) - GitHub deploys herewordpress/(symlink) - Pressable's shared WordPress core (managed by Pressable, read-only)
Test on staging first, then promote to production:
feature branch ──► staging branch ──► staging site (test)
│
▼ (open PR, merge)
main branch ──► production (live)
- Make changes locally on a feature branch
- Run
npx gruntto build assets (CSS/JS) - Commit (including built assets in
wp-content/plugins/bd4d/assets/) - Staging: push the work to the
stagingbranch → staging site auto-deploys → test. (Staging is gated by HTTP Basic Auth — log in with any WP admin credential — and it writes to the production Airtable base, so test submissions land in prod data. Delete them after.) - Production: open a PR to
main, merge (squash/rebase — linear history is enforced onmain) → production auto-deploys within minutes - Verify on production
Recommended: Always backup before deploying (see backup.sh in repo root)
Best for: When staging and production should be identical mirrors (content, settings, everything).
- Make and test changes on staging
- When satisfied → Clone staging to production in Pressable dashboard
Pros: Single source of truth, staging exactly matches what goes to production Cons: Copies EVERYTHING (database, uploads, settings) - overwrites any production-only content
| Scenario | Recommended |
|---|---|
| Plugin changes only | Option A (GitHub deploy) |
| Staging/production are identical mirrors | Option B (Clone) |
| Production has unique content or settings | Option A (GitHub deploy) |
| Unsure | Option A (safer) |
Note: Form submissions go to Airtable, not WordPress, so no form data is lost either way.
As of 2026-07-19 the staging branch is the deploy source for the staging site (it was
previously stale and unused). It was fast-forwarded to main, and the staging site's GitHub
Integration was pointed at it (Selected Branch: staging). Push feature work to staging to
deploy it to the staging site for testing; it doesn't need to stay perfectly in sync with main
between tests.
First-push note: the staging integration was (re)configured on 2026-07-19 after previously pointing at a deleted branch. The auto-deploy-on-push behavior is expected to work like production but should be confirmed on the first real push to
staging(check the deployed file timestamps on the server). If it doesn't fire, deploy manually via Set and Deploy or rsync, and re-check the integration.
- wpcs.yml - Validates WordPress-VIP-Go coding standards on PRs
- check-commits.yml - Enforces conventional commit messages
- Build tools (Grunt, npm, composer configs) live at repo root but are NOT deployed
- Only the
wp-content/plugins/bd4d/folder is deployed to Pressable - Built assets must be committed (they're not built on the server)
- Never deploy the entire
wp-content/folder - it will delete themes and other plugins not in this repo
Last updated: Jan 2026