Built for Braven Agency

A self-select lead router, built the WordPress way.

This documents a working proof-of-concept of Braven's Milestone 1 deliverable — a guided self-selection tool that routes cities, counties, chambers, and foundations to the right next step, captures the lead cleanly, and pushes it to the CRM, an email workflow, and GA4. It's a real WordPress plugin on your exact stack: WordPress + Elementor + custom PHP (CPTs / ACF) + GTM/GA4 + webhooks. No page-builder bloat.

Overview

The brief was specific: not a strategist, not an AI-tool builder — a WordPress engineer who ships a working, converting, fast lead-routing tool and can prove it. This build is that tool, plus the tracking layer and the CPT-backed video repository, delivered as one cohesive, dependency-light plugin.

What the visitor experiences

1 · Who are you?City · County · Chamber · Foundation · Corporate
2 · Your goalAI · Digital Marketing · Social · Full program
3 · Three qualifiersFunding · Timeline · Role · Scale
4 · Routed pathBook · Proposal · Funding · Nurture

Each answer feeds a transparent scoring engine. The visitor lands on a tailored view — their organization type and goal reflected back, the right call-to-action, and only the form fields that step actually needs. On submit, the lead is stored, sent to the CRM with retries, emailed to the team, acknowledged to the prospect, and recorded in GA4.

Milestone 1 — the router

The self-select flow, the scoring engine, and clean CRM + email capture. The primary deliverable.

Milestone 2 — tracking

GTM dataLayer events at every step + a redundant server-side GA4 conversion. Full-funnel measurability.

Milestone 4 — video library

A categorized, searchable training repository on CPTs + taxonomies. No directory plugin, no bloat.

Phase 2 — AI hooks

Clean action hooks, REST endpoints, and data structures so your AI team layers personalization in without a rebuild.

Architecture: a testable core, a thin WordPress skin

The one decision that makes everything else clean: the routing brain has no WordPress dependency. It's plain PHP that takes a selection and returns a decision. WordPress — CPTs, REST, Elementor, the admin — is a thin adapter layer wrapped around it. That's why the whole routing contract is unit-tested with php tests/test-routing.php (26 assertions, zero WordPress boot) and why the logic is trivial to reason about.

PUREclass-blr-routing-engine.php — score answers, pick a tier, resolve the outcome. The conditional logic.
PUREclass-blr-lead-validator.php — sanitise + validate a submission (honeypot, consent, required fields).
PUREclass-blr-webhook-dispatcher.php — build the CRM payload, sign it, retry, log. (One optional wp_remote_post seam, falls back to cURL.)
PUREclass-blr-ga4.php — build + send the GA4 Measurement Protocol event.
WPclass-blr-cpt.php · -fields · -rest · -shortcode · -elementor-widget · -admin · -lead-repository — bind the core to WordPress.
WPclass-blr-core.php — the bootstrapper that wires it all on plugins_loaded.
Why it matters to you: when a marketer wants a new buyer type or a re-pointed destination, they edit data/routes.php — a declarative matrix — and nothing in the engine changes. When an engineer wants to verify a change, the tests run in a second with no database. New buyer segments, new scoring, new outcomes: config edits, not rebuilds.

How the custom code fits inside a standard WordPress install

Everything below is stock WordPress — no forks, no core hacks, no exotic dependencies. It's a single plugin folder in wp-content/plugins/ plus a child theme. Drop it into any WordPress 6.4+ site and activate.

The plugin folder

wp-content/plugins/braven-lead-router/
  braven-lead-router.php        # plugin header, constants, activation hooks
  includes/
    class-blr-routing-engine.php # PURE — the decision brain
    class-blr-lead-validator.php # PURE — sanitise + validate
    class-blr-webhook-dispatcher.php # PURE* — CRM webhook, retries, HMAC
    class-blr-ga4.php            # PURE — server-side GA4 Measurement Protocol
    class-blr-email-workflow.php # wp_mail + blr_lead_captured hook
    class-blr-lead-repository.php # persist a lead as the blr_lead CPT
    class-blr-cpt.php            # register CPTs + taxonomies + custom table
    class-blr-fields.php         # register_post_meta + ACF integration
    class-blr-rest.php           # register_rest_route — /braven/v1/*
    class-blr-shortcode.php      # [braven_lead_router], [braven_video_library]
    class-blr-elementor-widget.php # the same tool as an Elementor block
    class-blr-admin.php          # columns, meta box, Routing Console, Settings
    class-blr-core.php           # bootstrapper
    helpers.php
  data/routes.php                # the routing matrix (editable config)
  data/videos.php                # seed catalog for the library
  templates/                     # server-rendered markup
  assets/css · assets/js         # ~6KB CSS, ~7KB JS, no jQuery
  acf-json/                      # ACF Pro field-group exports
  readme.txt · uninstall.php

The mapping from "custom features" to "standard WordPress primitives" is one-to-one:

FeatureWordPress primitiveWhere
Captured leadsCustom Post Type blr_lead + post metaclass-blr-cpt / -lead-repository
Training videosCPT blr_video + taxonomies blr_track, blr_proficiencyclass-blr-cpt
CRM delivery logCustom table via dbDelta()class-blr-cpt
Editable lead fieldsACF Pro field group (or native meta box fallback)acf-json / class-blr-admin
Wizard & capture APIregister_rest_route()class-blr-rest
Placement on a pageShortcode and Elementor widgetclass-blr-shortcode / -elementor-widget
Operator consoleAdmin submenu + custom list columnsclass-blr-admin
Trackingwp_head dataLayer + wp_remote_post to GA4class-blr-core / -ga4
ExtensibilityAction hook blr_lead_capturedclass-blr-email-workflow

Custom Post Types & taxonomies

Leads and videos are Custom Post Types — the idiomatic WordPress way to get an admin UI, search, capabilities, REST, and (crucially for your stack) ACF Pro compatibility for free. Videos add two hierarchical taxonomies so terms stay indexed and filterable.

register_post_type( 'blr_lead', [
  'public' => false,        // leads are private — never a public URL
  'show_ui' => true,        // but fully manageable in wp-admin
  'show_in_rest' => false,  // PII never exposed over public REST
  'supports' => [ 'title' ],
] );

register_taxonomy( 'blr_track', 'blr_video', [ 'hierarchical' => true, 'show_admin_column' => true ] );
register_taxonomy( 'blr_proficiency', 'blr_video', [ 'hierarchical' => true, 'show_admin_column' => true ] );

A custom table — where a CPT is the wrong tool

Leads are a CPT; the webhook delivery log is not. It's high-volume, append-only audit data — one row per CRM send attempt — so it lives in a real table created with dbDelta() on activation. Right tool per job; this is the "custom database structures" the Build Test asks about.

CREATE TABLE {$wpdb->prefix}blr_deliveries (
  id BIGINT UNSIGNED AUTO_INCREMENT,
  lead_id BIGINT UNSIGNED, idem_key VARCHAR(64),
  attempt SMALLINT, http_status SMALLINT,
  result VARCHAR(20), message VARCHAR(255),
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id), KEY lead_id (lead_id), KEY idem_key (idem_key)
);

ACF Pro — a first-class citizen, never a hard dependency

The plugin works with or without ACF. Without it, meta is registered and typed via register_post_meta() and the admin renders a native read-only panel. With ACF Pro active — your stack — the shipped field groups in acf-json/ take over the editing UI, bound to the same _blr_* meta keys the engine already reads. Zero migration: ACF simply upgrades the interface.

// class-blr-fields.php — point ACF's loader at our shipped field groups
add_filter( 'acf/settings/load_json', function( $paths ){ $paths[] = BLR_DIR . 'acf-json'; return $paths; } );

Two groups ship: group_blr_lead.json (name, org, email, phone, buyer type, track, intent tier/score, outcome, CRM status) and group_blr_video.json (video URL, duration).

REST API

The wizard talks to three endpoints under the braven/v1 namespace. Routing is separated from capture so the tailored view can render (and fire the routed dataLayer event) before asking for a single piece of PII — lower friction, and it lets you A/B the routed copy without resubmitting contact details.

EndpointMethodPurposeAuth
/braven/v1/configGETbuyer types, tracks, qualifierspublic (no PII)
/braven/v1/routePOSTselection → routing decisionpublic (no PII)
/braven/v1/leadPOSTcapture: CPT + CRM + email + GA4REST nonce + honeypot
register_rest_route( 'braven/v1', '/lead', [
  'methods' => 'POST',
  'callback' => [ 'BLR_REST', 'post_lead' ],
  'permission_callback' => '__return_true', // gated inside by nonce + honeypot
] );
Never trust the client. On capture the server recomputes the routing decision from the raw selection — the browser's claimed outcome is ignored — then validates, persists, and fans out. The client is a convenience, not an authority.

Shortcode and Elementor — one renderer

The job spec's exact principle: keep the deep logic in custom PHP; keep Elementor clean and usable for marketers. So there is one renderer, exposed two ways. Marketers drag the “Braven Lead Router” widget onto any Elementor page (with heading/intro controls); developers or a classic page use [braven_lead_router]. The Elementor widget carries no logic — it calls the shortcode renderer.

// class-blr-elementor-widget.php — the widget is a thin wrapper
protected function render() {
  $s = $this->get_settings_for_display();
  echo BLR_Shortcode::render_router( [ 'heading' => $s['heading'], 'intro' => $s['intro'] ] );
}

Assets enqueue only on pages that actually contain the tool — nothing loads site-wide.

Admin surfaces

  • Partner Leads — the blr_lead list with custom columns: buyer type, track, intent tier + score, and a colour-coded CRM status badge. Triage from the list without opening a record.
  • Routing Console — a dashboard page: leads by intent tier + the live CRM webhook delivery log.
  • Settings — CRM webhook URL & secret, GA4 Measurement ID + API secret, GTM container ID, booking URL, lead-magnet URL, notification email. No code edits to reconfigure.

Theme & GTM

Presentation is a hello-elementor child theme — your live theme — that loads Braven's fonts (Nunito Sans + Playfair Display) and palette (gold #c7945b, warm ink, cream). GTM is printed into wp_head only when a container ID is set, seeding the same dataLayer the wizard pushes to. On your site you'd set the ID to your existing GTM-535B5NR container and the events flow straight in.

The engine

Routing logic — transparent and auditable

The screening question is: walk us through the conditional logic. Here it is in full. Every qualifier answer carries a score. The sum, plus a small weight for higher-value segments, places the lead in a tier. A short, ordered rule list turns (buyer type, tier, funding posture) into an outcome — first match wins, with a safe default so no lead is ever dropped.

1 · Score the answers

QualifierAnswers → points
FundingFunded 40 · Budgeted 25 · Seeking 10 · Exploring 0
TimelineThis quarter 30 · 6 months 20 · This year 10 · Unsure 0
RoleApproves budget 20 · Recommends 12 · Researching 4
Scale500+ 20 · 100–500 14 · 25–100 8 · <25 3
Segment weightCity/County/Foundation +10 · Chamber/Corporate +5

2 · Map score → tier

Tier A · High intent ≥ 70   Tier B · Qualified 35–69   Tier C · Nurture < 35

3 · Resolve the outcome (first match wins)

#ConditionOutcomeNext step
1Foundation + funding = seeking/exploringFunding partnershipFunding intake + playbook
2Tier ABook a callBooking link + short confirm form
3Tier BRequest a proposalFuller intake (adds goals)
4Tier C (default)NurtureProgram overview + newsletter

The decision also attaches tailored copy — the buyer type's value proposition and the track label — so the routed view is never a generic form. Example decisions from the test suite: a funded, decision-making city training 500+ businesses (score 120) → Book a call; a budgeted county recommender (score 60) → Request a proposal; an exploring chamber (score 12) → Nurture; a foundation seeking funding → Funding partnership (the special rule beats the tier).

// data/routes.php — outcomes are config, not code
'rules' => [
  [ 'when' => [ 'buyer_type_in' => ['foundation'], 'funding_in' => ['seeking','exploring'] ], 'outcome' => 'funding_partnership' ],
  [ 'when' => [ 'tier' => 'A' ], 'outcome' => 'book_call' ],
  [ 'when' => [ 'tier' => 'B' ], 'outcome' => 'request_proposal' ],
  [ 'when' => [ 'tier' => 'C' ], 'outcome' => 'nurture' ],
],

The capture funnel

One POST to /braven/v1/lead fires the whole funnel, in order:

Validatesanitise · honeypot · consent · recompute decision
Persistblr_lead CPT + meta (dedup by idem key)
CRMwebhook + retries + log
Emailteam alert + prospect reply + hook
GA4server-side generate_lead

Each stage degrades safely: a failed email never fails capture; an unconfigured CRM is recorded, not thrown. The lead is captured no matter what downstream does.

CRM handoff — Smrts-shaped, idempotent, retried

The lead POSTs to a configurable webhook — your Smrts CRM inbound hook, or a Zapier/Make catch-hook. The payload is CRM-neutral and signed; delivery is idempotent and retried; every attempt is logged to the custom table and surfaced in the Routing Console.

Payload → Smrts field mapping

Payload pathMeaningMaps to (Smrts / CRM)
contact.email, .name, .phone, .organization, .titleContact identityContact/Lead record fields
segmentation.buyer_typecity / county / chamber / foundation / corporateLead source segment / list
segmentation.trackProgram interestInterest / campaign tag
segmentation.intent_tier + intent_scoreA / B / C + numericLead score / priority
segmentation.outcomeRouted next stepPipeline stage / workflow trigger
attributionUTM + gclidSource attribution fields
idempotency_keyStable per submissionDe-dupe key (upsert)

Reliability

  • Idempotency — every payload carries a 32-char key (also an X-Braven-Idempotency header), so a double-fired form or a retry collapses to one CRM record. The blr_lead CPT dedupes on the same key.
  • Signing — if a secret is set, the body is HMAC-SHA256 signed as X-Braven-Signature, so the CRM can verify authenticity.
  • Retries — transient failures (timeouts, 5xx, 429) retry with a short backoff; 4xx are terminal. Every attempt is logged with its HTTP status and result.
In this demo the webhook points at a local mock sink so you can see real delivered rows in the Routing Console end-to-end. In production you paste the Smrts inbound URL into Settings and delete the mock — no code change.

GA4 & the data layer

Two layers of measurement. The client pushes a clean event to dataLayer at every funnel step for GTM to pick up. The server independently fires the money event (generate_lead) via the GA4 Measurement Protocol on capture — so a conversion is recorded even when an ad-blocker or a bounce kills the client beacon. That server-side redundancy is the "server-side tagging a plus" line in your spec.

Client dataLayer events (wire these to GTM tags/triggers)

EventWhenKey parameters
blr_router_startTool loads
blr_buyer_type_selectedStep 1 chosenbuyer_type
blr_track_selectedStep 2 chosentrack
blr_qualifiers_answeredStep 3 completeanswers
blr_lead_routedRouted view shownbuyer_type, track, intent_tier, outcome
blr_lead_submittedCapture succeedsintent_tier, outcome, lead_id

The GA client id is read from the _ga cookie and passed to the server event, so the server-side conversion stitches to the same session in GA4. Recommended GTM setup: a GA4 Event tag per dataLayer event, keyed on a Custom Event trigger of the same name; map buyer_type, track, intent_tier, and outcome to registered GA4 custom dimensions.

Your three screening tests, answered

Speed Test — a searchable video library without tanking page load

My answer, and what this build does: do not reach for a directory plugin. Model videos as a blr_video CPT with two taxonomies (blr_track, blr_proficiency). Terms are indexed; one cached WP_Query renders every card into static-friendly markup on the server. Filtering is then pure client-side show/hide — no AJAX, no per-keystroke query, no round-trips. Search debounces over text already in the DOM.

How I hold PageSpeed > 90, concretely — by removing weight, not adding plugins:

  • Assets enqueue only where the tool/library is used (~6KB CSS + ~7KB JS, no jQuery, deferred).
  • The routing matrix is OPcache'd PHP config — no DB hit on the hot path.
  • Server-rendered markup means first paint doesn't wait on JavaScript.
  • Fonts preconnect + display=swap; images lazy-load; SVG icons inline (no icon font).
  • Elementor stays lean — it only loads its runtime on pages actually built with it, and the tool pages are shortcode/widget-rendered.

Routing Test — the conditional logic, form→CRM, and GA4

Covered in full above: the scoring & rules are a declarative matrix with a first-match resolver and a safe default; the form→CRM handoff is an idempotent, signed, retried webhook with a Smrts field map; each step's data pushes cleanly into GA4 client-side, with a redundant server-side conversion. The server always recomputes the decision — the client never dictates the outcome.

Accessibility — WCAG 2.1 AA for civic buyers

Your audiences are government and institutional, so accessible builds are non-negotiable. Built in from the start, not bolted on:

  • Each question is a <fieldset> with a <legend>; options are real <input type="radio"> controls inside a role="radiogroup" — fully keyboard operable, correctly announced by screen readers.
  • Visible focus states on every control (never outline:none without a replacement).
  • Step changes move focus to the new step and are announced via an aria-live region; errors are announced too.
  • Colour contrast meets AA (ink on paper, white on gold verified); state is never colour-only.
  • The wizard is server-rendered, so the questions exist without JavaScript; a <noscript> fallback offers a phone number.
  • prefers-reduced-motion disables the shake/entrance animations.

Performance summary

No framework

Vanilla JS + PHP. No jQuery, no React, no build step for the front-end.

Conditional assets

CSS/JS load only on pages with the tool. The rest of the site is untouched.

SQLite-friendly

The demo runs WordPress on SQLite — the whole thing is one container, no MySQL.

Cached & indexed

Config is OPcache'd PHP; taxonomy queries are cached; the library filters in-browser.

Operations

Phase 2 — clean seams for your AI team

The spec is explicit: your AI team owns the generative logic; I architect the hooks so it plugs in without a rebuild. The seams are already here:

  • Action hookdo_action( 'blr_lead_captured', $lead, $decision, $lead_id ) fires on every qualified lead. An AI feature (e.g. generate a custom syllabus) subscribes to it with zero changes to this plugin.
  • REST — the braven/v1 namespace is the natural home for a gated /syllabus or /worksheet endpoint that reads the same lead data structures.
  • Typed data — buyer type, track, tier, and answers are stored as structured meta, so a personalization layer has clean inputs to condition on.

Installing on bravenagency.com

  1. Upload braven-lead-router to wp-content/plugins/ and activate. (Activation registers the CPTs, creates the delivery table, and seeds the library.)
  2. Partner Leads → Settings: paste your Smrts CRM webhook URL (+ optional secret), GA4 Measurement ID + API secret, GTM ID (GTM-535B5NR), booking URL, and lead-magnet URL.
  3. Add the tool to any page: drop the Braven Lead Router Elementor widget, or use [braven_lead_router]. Add [braven_video_library] for the library.
  4. ACF Pro is auto-detected; the shipped field groups appear on the Lead and Video editors — no import needed (they load from acf-json/).
  5. Point the child theme at your live fonts/logo if different; nothing else to configure.
Because leads are a CPT and the webhook is idempotent, you can install alongside your existing forms and cut traffic over gradually — no big-bang migration.

Security

  • CSRF — the capture endpoint requires a valid WordPress REST nonce.
  • Spam — a hidden honeypot field; bots that fill it get a silent 200 and nothing is stored.
  • Input — every field is sanitised at one choke point; email is validated; output is escaped.
  • Privacy — leads are a private CPT with show_in_rest => false; PII is never exposed over public REST. Meta auth callbacks require edit_posts.
  • Webhook integrity — optional HMAC-SHA256 signature so the CRM can verify each payload.

Built as a working reference for Braven Agency · Levelbrook · Source and tests included · Live demo