Skip to content

Contact forms and enquiries

Every enquiry on the site goes through Contact Form 7, wrapped in a theme layer that picks the right form per page type, injects honeypots, populates dropdowns from live content, pre-selects the visitor’s practice area, and tags submissions with the Google Analytics client ID.

Enquiries are the site’s primary business outcome — if this breaks, leads are lost silently.

The firm needs different forms for different journeys (general enquiry, careers application, event registration) but does not want editors wiring a form per page. So the form block resolves the form from page context and from Global Options, and the theme injects the shared anti-spam and analytics plumbing into every form regardless of how it was built in the CF7 UI.

The service dropdown is generated from service posts rather than typed into CF7 so it cannot drift from the site’s actual practice areas.

Path What it does
wp-content/themes/bpcollins/blocks/form-block/block-form-block.php Chooses the form for the current page and renders the shortcode
wp-content/themes/bpcollins/inc/cf7.php Honeypot form tag, spam filter, GA client ID capture, conditional property fields
wp-content/themes/bpcollins/inc/template-functions.php:336 pine_dynamic_select_field_values() — populates the services select from service posts
wp-content/themes/bpcollins/inc/template-functions.php:387 pine_preselect_parent_service_in_cf7_dropdown() — pre-selects the current practice area
wp-content/themes/bpcollins/inc/template-functions.php:495 dynamic_careers_field() — populates the careers select from career posts
wp-content/themes/bpcollins/inc/template-functions.php:528 dynamic_events_field() — populates the events select from posts in the events category
wp-content/themes/bpcollins/inc/classes/BlockMeta.php:163 render_form() — renders a CF7 form from any block’s form field

Form selection (block-form-block.php:11-39), first match wins:

Context Heading option Form option
career single careers_form_title careers_form
events category or post in it events_form_title events_form
any category, News page, any taxonomy archive services_form_title services_form
post / news single services_form_title services_form
service single services_form_title per-post services_form, falling back to the global
sector single sectors_form_title sectors_form

Anywhere else the block uses its own form field. The section id is apply on careers, form on any other singular, otherwise the block’s block_id — that is what in-page “Enquire now” anchors target.

The form is rendered as a shortcode string with three attributes: [contact-form-7 services="<top-level service title>" id="<id>" title="<title>"] (block-form-block.php:111). The non-standard services attribute survives because custom_shortcode_atts_wpcf7_filter() whitelists it (inc/template-functions.php:629).

Dynamic dropdowns. wpcf7_form_tag filters rewrite the option list of named tags at render time:

  • services — all top-level service posts (up to 200) plus a hardcoded “Notary Services” entry, sorted alphabetically, with a leading blank option (inc/template-functions.php:336-379).
  • careers — up to 20 career posts by title.
  • events — up to 20 posts in the events category.

Practice-area pre-selection. On a service single, pine_preselect_parent_service_in_cf7_dropdown() prints an inline script in wp_footer that finds #service-select, matches the top-level ancestor’s title by option value or text, sets it, dispatches a change event, and syncs the visible nice-select wrapper — retrying every 150ms up to 25 times because CF7 and nice-select initialise late (inc/template-functions.php:474-487).

Conditional property fields. strategiq_cf7_enqueue_conditional_fields_script() (inc/cf7.php:251) attaches an inline script to app-scripts that shows the property-services select only when the Property service is selected — or when the visitor is on a /services/property/ URL and has not yet touched the dropdown — and shows the residential field groups only when property - residential is chosen. On Property service pages it also injects a CSS rule so the group is visible before JS runs (inc/cf7.php:271-277).

Anti-spam. Three layers:

  1. add_form_shortcodes() (inc/cf7.php:142) appends [honeypot your-firstname], [honeypot your-mobilephone], [honeypot your-companyname] and [form_title] to every form’s markup unless already present. The honeypot tag type is registered at inc/cf7.php:135 and renders a visually hidden, tabindex="-1" text input.
  2. wpcf7_honeypot_validation_filter() (inc/cf7.php:95) fails validation as spam if any honeypot field arrives non-empty or missing.
  3. strategiq_cf7_enquiry_check_spam_filter() (inc/cf7.php:201) marks the submission as spam when any posted field whose name contains enquiryCheck is non-empty, and logs form_id and field name via error_log.

Analytics tagging. strategiq_cf7_add_ga_cid() (inc/cf7.php:186) parses the _ga cookie for the GA client ID and appends it to posted data as ga-cid, so an enquiry can be joined back to its GA session. [form_title] injects the form’s title as a hidden form-type field (inc/cf7.php:119-128), which is what distinguishes submissions in the notification email.

wpcf7_autop_or_not is disabled (inc/cf7.php:3) so form markup is exactly what the editor wrote.

flowchart TD
A[page renders form-block] --> B[context → which form + heading]
B --> C["do_shortcode contact-form-7 services=… id=…"]
C --> D[wpcf7_form_tag filters fill services/careers/events options]
C --> E[add_form_shortcodes appends honeypots + form_title]
D --> F[footer script pre-selects the practice area]
F --> G[inline script shows conditional property fields]
H[submit] --> I[honeypot validation]
I --> J[enquiryCheck spam filter]
J --> K[wpcf7_posted_data adds ga-cid]
K --> L[CF7 mail]

All in the Global Options ACF page (inc/acf.php:18) unless noted:

  • careers_form / careers_form_title
  • events_form / events_form_title
  • services_form / services_form_title (also settable per service post)
  • sectors_form / sectors_form_title

Per-block: form, heading_text, copy, block_id.

Recipients, subjects and mail bodies live in the Contact Form 7 admin, not in the theme. Contact Form 7 is a required plugin (inc/required-plugins/required-plugins.php).

  • A missing honeypot field is treated as spam. wpcf7_honeypot_validation_filter() fails when the field is absent, not just when filled (inc/cf7.php:110). Any change that strips the injected honeypots — a caching layer rewriting the form, or an editor pasting form markup that bypasses add_form_shortcodes() — silently rejects every real submission as spam. Symptom: enquiries stop arriving while the form still says it sent.
  • add_form_shortcodes() detects honeypots by substring (strpos($_f, 'honeypot')). The word appearing anywhere in the form — even in a comment — suppresses injection of all three.
  • The services dropdown is matched by title, not ID. Pre-selection compares option value/text against the ancestor’s post_title (inc/template-functions.php:432), and the submitted value is a title string. Renaming a service changes what lands in the notification email and breaks pre-selection for existing links.
  • “Notary Services” is hardcoded into the services list (inc/template-functions.php:363) because it has no service post.
  • Property conditional logic is matched on labels. It looks for the literal strings property, property - residential, property-residential (inc/cf7.php:358-366), and for the URL fragment /services/property/ (inc/cf7.php:354). Renaming that service or its options disables the conditional fields with no error.
  • Two separate inline scripts touch the same select — pre-selection from wp_footer and conditional fields from app-scripts. The conditional script tracks whether the change was user-initiated via e.isTrusted (inc/cf7.php:420) specifically so the programmatic pre-select does not count as a user choice. Preserve that if you touch either.
  • Timing is handled by retry, not by events (25 × 150ms in one script, setTimeout ladders in the other). It works but it is fragile under slow loads; new logic should hook wpcf7init as initAll() does.
  • strategiq_honeypot_has_parent_plugin() calls deactivate_plugins(plugin_basename(__FILE__)) on a theme file (inc/cf7.php:16) — a copy-paste from the honeypot plugin it was derived from. It only runs if CF7 is inactive, and deactivating a theme path is a no-op, but it is not doing what it reads as doing.
  • Spam blocks are logged to PHP error log, not to an admin screen — check the host’s error log when investigating “we never got the enquiry”.
  • New form for a new page type → add the ACF option pair, then add a branch to the chain in block-form-block.php:11-39 in the right order (earlier branches win).
  • New dynamic dropdown → copy the dynamic_careers_field() pattern: filter wpcf7_form_tag, bail unless $scanned_tag['name'] matches, build raw_values as value|label, then rebuild values/labels/pipes through WPCF7_Pipes. Skipping the pipes rebuild produces a select with no labels.
  • New non-standard shortcode attribute → whitelist it in custom_shortcode_atts_wpcf7_filter() (inc/template-functions.php:629) or CF7 drops it.
  • Changing anything about honeypots → test a real submission end to end and confirm the email arrives. A honeypot mistake fails closed and is invisible from the front end.
  • Deliberately not abstracted: the honeypot implementation is a vendored copy of the CF7 Honeypot plugin rather than a dependency, so the plugin need not be installed. Do not “replace it with the plugin” without checking every form.
  • Verify: submit each form type (general, careers, events, a Property service page) and confirm the email arrives, carries the right form-type, and includes ga-cid when a GA cookie is present.

None. No automated tests exist in this repository. Form changes must be verified by real submissions on staging.