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.
Why it exists
Section titled “Why it exists”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.
Entry points
Section titled “Entry points”| 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 |
How it works
Section titled “How it works”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-levelserviceposts (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 20careerposts by title.events— up to 20 posts in theeventscategory.
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:
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 atinc/cf7.php:135and renders a visually hidden,tabindex="-1"text input.wpcf7_honeypot_validation_filter()(inc/cf7.php:95) fails validation as spam if any honeypot field arrives non-empty or missing.strategiq_cf7_enquiry_check_spam_filter()(inc/cf7.php:201) marks the submission as spam when any posted field whose name containsenquiryCheckis non-empty, and logsform_idand field name viaerror_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]Configuration
Section titled “Configuration”All in the Global Options ACF page (inc/acf.php:18) unless noted:
careers_form/careers_form_titleevents_form/events_form_titleservices_form/services_form_title(also settable perservicepost)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).
Invariants and gotchas
Section titled “Invariants and gotchas”- 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 bypassesadd_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
servicesdropdown is matched by title, not ID. Pre-selection compares option value/text against the ancestor’spost_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 noservicepost. - 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_footerand conditional fields fromapp-scripts. The conditional script tracks whether the change was user-initiated viae.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,
setTimeoutladders in the other). It works but it is fragile under slow loads; new logic should hookwpcf7initasinitAll()does. strategiq_honeypot_has_parent_plugin()callsdeactivate_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”.
Changing it safely
Section titled “Changing it safely”- 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-39in the right order (earlier branches win). - New dynamic dropdown → copy the
dynamic_careers_field()pattern: filterwpcf7_form_tag, bail unless$scanned_tag['name']matches, buildraw_valuesasvalue|label, then rebuildvalues/labels/pipesthroughWPCF7_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 includesga-cidwhen a GA cookie is present.
None. No automated tests exist in this repository. Form changes must be verified by real submissions on staging.