A WordPress lead pipeline built the way I'd ship it for production: capture a lead, score it, store it, and sync it out to a CRM and to HubSpot without ever blocking the visitor or losing a lead when a downstream service is down.
This repo holds only the custom code (MU-plugins, a child theme, ACF exports, tests, CI, docs). WordPress core and third-party plugins are git-ignored, so bring your own WordPress: drop
wp-content/into an install, or use the setup steps below.
NimbusOps is a fictional SaaS company. A visitor submits the demo form, and the pipeline:
- Validates and scores the lead (corporate domain, company, plan, message length).
- Writes it to an append-only
wp_nimbusops_leadstable. - Fires a
nimbusops/lead/capturedevent. - Two listeners react, independently of each other:
- FluentCRM (in-process): upserts the contact, tags it by score band, lets a funnel take over.
- n8n (async): posts a signed webhook that retries with backoff, which upserts the contact in HubSpot.
The FluentCRM hop runs inline because it's local. The network hop to n8n is scheduled, so a slow or down n8n never slows the form response.
| Area | Where |
|---|---|
| MU-plugin architecture, deterministic load order | nimbusops-loader.php + nimbusops/ |
| Unit-testable business logic pulled out of the framework | lib/scoring.php + tests/ (29 tests) |
| Endpoint hardening: nonce, capability, honeypot, rate-limit, sanitize/escape | form-logger.php |
Custom $wpdb table via dbDelta with proper indexes |
form-logger.php |
| Event-driven design (listeners don't know about each other) | nimbusops/lead/captured + bridges |
| Async jobs, HMAC-signed webhooks, retry with backoff, idempotency | n8n-dispatcher.php |
| CRM integration: idempotent upsert, score-to-tag mapping, consent/double opt-in | fluentcrm-bridge.php |
| CPT + taxonomy, ACF as version-controlled JSON, JSON-LD + SEO meta | cpt.php |
| Dependency-free front end, a11y, RTL-ready | shortcodes.php + child theme |
| CI: lint + test matrix on PHP 8.2/8.3/8.4 | .github/workflows/ci.yml |
- WordPress 7.0, PHP 8.2+, MySQL 8.0+ / MariaDB 10.6+
- FluentCRM (on-site CRM), n8n (orchestration via signed webhook), HubSpot (synced from n8n)
- ACF (field groups as local JSON), Hello Elementor parent + a child theme, LiteSpeed Cache
wp-content/
├── mu-plugins/
│ ├── nimbusops-loader.php # the only auto-loaded file; requires modules in order
│ └── nimbusops/
│ ├── config.php # constants, logger, secret resolution
│ ├── lib/scoring.php # pure logic, no WP, unit-tested
│ ├── form-logger.php # leads table + REST endpoint + scoring + admin viewer
│ ├── cpt.php # Solution CPT + taxonomy + ACF JSON + JSON-LD/SEO
│ ├── shortcodes.php # lead form, solutions grid, pricing table
│ └── integrations/
│ ├── fluentcrm-bridge.php # lead event -> FluentCRM (sync)
│ └── n8n-dispatcher.php # lead event -> signed n8n webhook (async, retrying)
├── themes/nimbusops-child/ # styles + single-Solution template
└── acf-json/ # ACF field group export
tests/ 29 PHPUnit tests (scoring + name parsing)
docs/ hubspot-mapping, automation-flows, performance
n8n/ importable workflow scaffold + setup notes
scripts/ test-lead-capture.sh, healthcheck.sh, seed-demo.php
WordPress only auto-loads top-level files in mu-plugins/, alphabetically.
nimbusops-loader.php is the single entry point and requires the nimbusops/
modules in a fixed order, so nothing depends on filename luck.
Add the shortcodes to any page (Elementor, block editor, or classic):
[nimbusops_lead_form title="Request a demo" source="homepage"]
[nimbusops_solutions count="6" category="automation"]
[nimbusops_plan_table]
The form posts to POST /wp-json/nimbusops/v1/lead with a wp_rest nonce.
Captured leads show under Tools > NimbusOps Leads. Full flow in
docs/automation-flows.md.
The scoring and name-parsing logic lives in
lib/scoring.php with no
WordPress dependency, so it's unit-tested directly:
composer install && composer test
# or, without Composer:
php phpunit.phar --bootstrap tests/bootstrap.php tests/Unit
# OK (29 tests, 31 assertions)CI runs the suite plus a php -l lint of every custom file on PHP 8.2 / 8.3 / 8.4.
The WordPress-coupled paths are exercised with
scripts/test-lead-capture.sh and
scripts/healthcheck.sh against a running site.
You need a WordPress 7.0 site (XAMPP, LocalWP, or any host). Then:
- Copy
wp-content/mu-plugins/,wp-content/themes/nimbusops-child/, andwp-content/acf-json/into the install. The mu-plugins auto-load. - Install and activate Elementor (Hello Elementor parent), ACF, FluentCRM, LiteSpeed Cache, then activate the NimbusOps Child theme.
- Copy
.env.exampleto.env(or set the constants inwp-config.php) and setNIMBUSOPS_N8N_WEBHOOK_URLandNIMBUSOPS_N8N_SIGNING_SECRET. - Map FluentCRM tag ids:
wp option update nimbusops_fluentcrm_tag_map '{"hot":12,"warm":13,"cold":14}' --format=json - Import
n8n/nimbusops-lead-to-hubspot.workflow.jsoninto n8n and wire credentials (see n8n/README.md).
Optional WP-CLI quickstart (from the project root, with wp-cli.phar present):
php wp-cli.phar config create --dbname=nimbusops --dbuser=root --dbpass=''
php wp-cli.phar core install --url="http://localhost/nimbusops" --title="NimbusOps" \
--admin_user=admin --admin_password=admin123 --admin_email=admin@example.com --skip-email
php wp-cli.phar plugin install elementor advanced-custom-fields fluent-crm litespeed-cache --activate
php wp-cli.phar theme install hello-elementor
php wp-cli.phar theme activate nimbusops-child
php wp-cli.phar eval-file scripts/seed-demo.php
php wp-cli.phar rewrite structure '/%postname%/' --hardNotes:
- The
benefitsACF field is a repeater, which needs ACF Pro. The other fields and the template work fine on free ACF. - A lead's
sync_statusstayspendinguntilNIMBUSOPS_N8N_WEBHOOK_URLis set. The FluentCRM sync runs either way. - Elementor's Cloud Library fails to load on a dotless host (
localhost) with a long sub-path. Use a short URL likehttp://localhost/nimbusopsor a.testvhost and it loads fine.
Never commit wp-config.php or .env. Secrets live in constants or environment,
never in tracked files. The lead endpoint enforces nonce, honeypot, rate-limit, and
input sanitization; admin output is escaped.
MIT for the custom code in this repo. Premium plugins (Elementor Pro, ACF Pro) need their own licenses if you use them.
Lotfi O. · GitHub · oldev.site · LinkedIn