WhatAbility — Analytics, Attribution & Lead Tracking

Master Implementation Document

Sitewhatability.com.au
ScopeWeb analytics, ad attribution, lead tracking, loop closure
Version1.3 — §19 Compliance, Privacy & Consent added (APP gap analysis, intake form, cookie disclosure)
Date22 May 2026
Supersedeswhatability-closing-the-loop, whatability-analytics-tracking, whatability-data-assessment, whatability-datalayer, data-layer-briefing, WhatAbility-GA4-Audit-Checklist, whatability-datalayer-audit
AudienceMarketing, operations, web developer, leadership
PurposeSingle source of truth for analytics, attribution and lead tracking — readable as both technical documentation and operational playbook.

0. How to Use This Document

This document merges four earlier working documents into one. Read it in the order it is written — sections build on each other. Different readers need different parts:

  • Leadership / non-technical — Sections 1, 2, 13 (loop closure), 18, 19.
  • Marketing team — Sections 7–12, 15, 17.
  • Web developer — Sections 4–6, 13, 14, 16.
  • Whoever owns rollout — Sections 17 and 18 are the action list.

Diagrams are written in Mermaid and render in GitHub, Obsidian, VS Code and most Markdown viewers.

1. Executive Summary

WhatAbility runs paid campaigns (Google Ads, Meta) across five states — Sydney, Perth, Melbourne, Brisbane, Gold Coast — promoting NDIS services. The business needs to answer two questions it currently cannot:

  1. Which location and service combinations generate leads — and from which channel and campaign.
  2. Which leads become confirmed clients — closing the loop from ad spend to actual revenue.

The fix has three parts:

  1. Install the tracking foundation — GTM, GA4, Meta Pixel, Google Ads conversions.
  2. Add a structured data layer — so every page and every conversion carries service, location and campaign context.
  3. Close the loop — connect confirmed-client outcomes (in Supportmate / the intake process) back to the marketing data.

Parts 1 and 2 are achievable in roughly 4–6 weeks. Part 3 is staged over the following months. Even before the loop is closed, the data layer transforms visibility: from "we got 43 enquiries" to "43 enquiries — 18 Community Access Sydney from Google Ads, 11 Perth Camps from Meta, 7 SIL Melbourne from organic."

Important — the four source documents assume a greenfield build ("no tracking installed, create a new GA4 property"). A separate GA4 audit (21 May 2026) found an existing GA4 property and GTM container already on the site, configured via the MonsterInsights plugin and carrying significant technical debt. This contradiction must be resolved before implementation begins — see Section 18, Risk R1.

2. Business Process & Lead Flow

2.1 The lead journey — five steps

A person who becomes a WhatAbility client passes through five steps. Tracking covers steps 1–3 automatically once implemented. Steps 4–5 happen inside the intake process and require deliberate connection (loop closure — Section 13).

StepWhat happensTracked byStatus
1. Ad clickPerson clicks a Google or Meta adUTM parameters → GA4✅ Automatic
2. BrowseLands on a service/location page, scrolls, navigatesData layer + GTM✅ Automatic
3. EnquirySubmits the Onboardify form, or taps the phone numberConversion events → GA4 + Google Ads + Meta✅ Automatic
4. QualificationTeam calls back, assesses NDIS eligibility and fitInternal process only❌ Gap
5. Confirmed clientPerson is onboarded into SupportmateRequires loop closure⚡ Needs connection

2.2 Lead entry points

All known ways a lead can enter the business. Today, none carry channel or page attribution.

Entry pointWhere on siteToolAttribution today
Main enquiry formAll service & location pages, /pages/get-support/Onboardify (multi-step, embedded)None
Phone callEvery page — 1300 358 7141300 virtual number (provider unknown)Call volume only
Event / camp signup/events/No booking tool — phone/emailFully manual
Support worker application/pages/support-worker/, /about/jobs/OnboardifyNone
SIL direct booking/services/supported-independent-living/Google Calendar booking linkNone
Welcome pack download/resources/participant-welcome-pack/Direct PDF link (ungated)N/A — not a lead capture

2.3 Diagram — lead flow across systems

flowchart LR A["Visitor
Google / Meta / Direct"] --> B["Website
whatability.com.au"] B --> C{"Lead capture
method"} C -->|Form| D["Onboardify
enquiry / application"] C -->|Phone| E["1300 358 714
call"] C -->|SIL| F["Google Calendar
booking"] C -->|Event| G["Phone / email
manual"] D --> H["Team qualifies
& follows up"] E --> H F --> H G --> H H --> I["Confirmed client
Supportmate"] classDef tracked fill:#e6f7ed,stroke:#16a34a,color:#14532d classDef gap fill:#fdecec,stroke:#dc2626,color:#7f1d1d classDef partial fill:#fff6e6,stroke:#b45309,color:#7c2d12 class A,B tracked class D,E,F,G partial class H,I gap

Green = trackable once tracking is live. Amber = data exists but is siloed or incomplete. Red = no visibility today; requires loop closure.

3. Tool Stack Overview

3.1 Stack map

ToolRoleStatusMarketing value
Google Tag ManagerTag deployment containerActiveGTM-MCXFB7T, injected via the Head & Footer Code plugin (clean-up — Appendix B)Deploys all other tags without developer involvement
GA4Web analyticsActive — property 250532328, configured via MonsterInsights (clean-up — Appendix A)Page traffic, sessions by source, conversions, location/service segmentation
Google AdsPaid search/displayActive (campaigns running)Conversion-based Smart Bidding once tracking is connected
Microsoft ClarityHeatmaps / session recordingActiveBehavioural insight on forms and pages — must be in consent scope (Appendix C)
Meta PixelMeta ad attribution + audiencesNot installedConversion attribution for Meta spend, remarketing audiences
OnboardifyLead capture / onboarding formsActiveLead volume; no source attribution today
SupportmateParticipant / client management (CRM equivalent)ActiveClient conversion milestone; no marketing source data today
HIVOPhoto / digital asset managementActiveNot relevant to lead generation
1300 number providerPhone lineActiveCall volume only; no source tracking
Google CalendarSIL booking linkActiveBooking count; no attribution
WordPressWebsite platform (Elementor / Elementor Pro builder)ActiveHosts GTM (via Head & Footer Code plugin) and the data layer (§5.7) — see Appendix C for the full plugin audit

3.2 Diagram — data flow between platforms

flowchart TD W["Website + Data Layer
WordPress"] --> GTM["Google Tag Manager"] GTM --> GA4["GA4 Property"] GTM --> META["Meta Pixel"] GTM --> GADS_RMK["Google Ads
remarketing tag"] GA4 -->|conversion import| GADS["Google Ads
conversions"] GA4 <-->|linked| GSC["Google Search Console"] GA4 -->|audiences| GADS GA4 -->|audiences| META ONB["Onboardify"] -.->|loop closure| SM["Supportmate"] SM -.->|confirmed client signal| GA4 SM -.->|offline conversion CSV| GADS SM -.->|offline conversion CSV| META classDef live fill:#e6f7ed,stroke:#16a34a,color:#14532d classDef todo fill:#fff6e6,stroke:#b45309,color:#7c2d12 class W,GTM,GA4 live class META,GADS,GADS_RMK,ONB,SM,GSC todo

Solid lines = core tracking pipeline. Dotted lines = loop-closure connections (later phases, Section 13).

4. Tracking Architecture

4.1 Principles

  1. GTM is the single installation. It is placed on the site once. Every other tag — GA4, Meta, Google Ads — deploys through GTM. The developer is needed once.
  2. The data layer is the source of truth for page context. Every page writes its service, location and type into a structured data layer object. GTM reads it; GA4, Google Ads and Meta all receive it.
  3. Conversions carry context. A conversion event is never just "a form was submitted" — it carries which service, which location, which campaign, and a dollar value.
  4. Accuracy over volume. A conversion that fires twice, or with a missing value, is worse than one that doesn't fire — it corrupts Smart Bidding. Validate before going live (Section 16).

4.2 Diagram — GTM / data layer event flow

flowchart TD subgraph Page["Page load"] P1["dataLayer.push
Layer 1 — page context
page_type, service_name, location_name"] end subgraph Action["User action"] A1["dataLayer.push
Layer 2 — conversion event
e.g. enquiry_submitted"] end P1 --> GTM["GTM container"] A1 --> GTM GTM --> V["GTM reads Data Layer Variables
dlv - page_type, dlv - service_name, etc."] V --> T1["GA4 Configuration tag
(All Pages)"] V --> T2["GA4 Event: page_context
(All Pages)"] V --> T3["GA4 Event: conversion
(on trigger)"] V --> T4["Meta Pixel event
(on trigger)"] T1 --> GA4["GA4"] T2 --> GA4 T3 --> GA4 T4 --> META["Meta"]

5. Data Layer Specifications

The data layer is a structured JavaScript object (window.dataLayer) that every page populates. It has three layers.

5.0 Current state of the data layer

A data-layer audit (21 May 2026, 5 key pages) found the data layer already initialised on every page — but extremely thin. Only three variables are pushed today:

Variable present todayCaptures
eventEvent name (e.g. a form-submission trigger)
formIdID of the submitted form
responseForm submission response

These exist solely to support form-submission tracking. There is no page, user, or business context — every page looks identical in GA4 reporting. Standard GTM events (page views, button clicks, scroll, video, downloads, errors) fire, but no custom events are defined and outbound-link tracking is off. The schema in §5.1–§5.4 is the target state that replaces this thin setup; see Appendix A for the full current-state audit.

5.1 Layer 1 — Page Context

Pushed on every page, before the GTM snippet. Values come from the URL or CMS template variables. Persists for the whole page session.

<script>
  window.dataLayer = window.dataLayer || [];
  dataLayer.push({
    // Page classification
    'page_type':      'service',            // see value table
    'content_group':  'service',            // GA4 content grouping

    // Service context (null unless a /services/* page)
    'service_name':   'community-access',   // slug
    'service_label':  'Community Access',   // human-readable

    // Location context (null unless a /locations/* page)
    'location_name':  null,                 // slug
    'location_state': null,

    // Debug / template tracing
    'page_template':  'service'
  });
</script>

page_type values

page_typeExample URLservice_namelocation_name
homepage/nullnull
service/services/community-access/service slugnull
location/locations/perth/nulllocation slug
about/about/, /about/our-team/, /about/our-story/nullnull
careers/about/jobs/, /pages/support-worker/, /pages/support-coordinators/nullnull
enquiry/pages/get-support/nullnull
event/event/{slug}/, /events/, /pages/event-waitlist/nullnull
blog/blog/, /blog/{slug}/nullnull
resource/resources/participant-photo-albums/nullnull
partner/partners/optus/nullnull
info/pages/feedback-and-complaints/, /privacy-policy/, /terms-of-use/nullnull
landing/autism-support-with-what-ability/ (paid-ad pages)nullnull
thankyou/thank-you-need-support/nullnull
other/venue/, /organiser/, test/template pagesnullnull
This taxonomy was verified against a full-site crawl (22 May 2026, 341 live pages — 100% classified). Three facts the original planning documents got wrong: - No combined service-location pages. Services (/services/{slug}/) and locations (/locations/{slug}/) are separate page trees — no /services/{service}/{location}/ URL exists. On-page service_name and location_name never co-occur. - No /contact/ page. Enquiries route through /pages/get-support/ (page_type: enquiry). - The Events Calendar dominates. 303 of 341 pages (89%) are /event/, /events/, /venue/, /organiser/. event is by far the largest page_type; /venue/ and /organiser/ are taxonomy pages mapped to other. The five service slugs are community-access, ndis-camps, ndis-respite, supported-independent-living, behaviour-support-plans. The five location slugs are sydney, perth, melbourne, brisbane, gold-coast.

5.2 Layer 2 — Conversion Events

Pushed when a conversion occurs. GTM automatically reads Layer 1 context, so only push the event-specific properties. Full event specifications are in Section 7.

<script>
  dataLayer.push({
    'event': 'enquiry_submitted',
    'form_type': 'get-support',           // get-support | contact | other
    'service_interest': 'community-access', // from form field OR page context
    'location_interest': 'sydney',
    'conversion_value': 250,
    'currency': 'AUD',
    'enquiry_id': 'ENQ-00123'             // optional — if Onboardify exposes it
  });
</script>
service_interest fallback — if the Onboardify form does not expose the chosen service, fall back to service_name from Layer 1 (the page the form was opened on). Accurate for single-service pages; will be null on the homepage or the general enquiry page (/pages/get-support/), which is acceptable.

5.3 Layer 3 — User Context

Optional enrichment for returning-user segmentation and first-touch attribution.

<script>
  dataLayer.push({
    'user_type': 'returning',              // new | returning
    'first_touch_source': 'google',
    'first_touch_medium': 'cpc',
    'first_touch_campaign': 'community-access_sydney'
  });
</script>
Never push PII (name, email, phone, DOB) to the data layer. GA4 rejects it and it creates compliance exposure under the Australian Privacy Act. First-touch implementation code is in Section 13.5.

5.4 Per-page implementation

The rules below are the URL → page-context mapping. Under Method A (recommended) the GTM JS - page_type function (§5.6) implements them; under Method B the §5.7 snippet does. Verified against a full-site crawl (22 May 2026).

URL patternpage_typeservice_namelocation_name
/homepagenullnull
/services/, /services/{service}/servicefrom slugnull
/locations/, /locations/{location}/locationnullfrom slug
/about/jobs/, /pages/support-worker/, /pages/support-coordinators/careersnullnull
/about/* (all other)aboutnullnull
/pages/get-support/enquirynullnull
/event/, /events/, /pages/event-waitlist/eventnullnull
/blog/*blognullnull
/resources/*resourcenullnull
/partners/*partnernullnull
/pages/* (all other), /privacy-policy/, /terms-of-use/, /incident-reporting/infonullnull
paid-ad landing pages (explicit slug list — §5.6)landingnullnull
/thank-you*thankyounullnull
/venue/, /organiser/, test/template pages, anything elseothernullnull

5.5 Implementation methods — overview

Layer 1 page context can be populated two ways. Both are documented in full — Method A in §5.6, Method B in §5.7 — so the implementer can choose based on access and the site's plugin setup (Appendix C).

MethodHow it worksFeeds GTM viaProsCons
A — GTM JavaScript variablesGTM Custom JavaScript variables parse service/location from the URL path{{JS - ...}} / {{LT - ...}} variablesNo WordPress change at all; lives entirely inside GTM (version-controlled, one-click rollback)Page context stays inside GTM — cannot reach server-side systems; service_label needs a Lookup Table variable (§5.6); URL-dependent
B — Real data layer pushA script pushes a real dataLayer object before GTM loads{{dlv - ...}} variables (§6.2)Real data layer; supplies every field; the §5.1 schema works as writtenRequires one WordPress change

Recommendation: Method A. It needs no change to WordPress — all logic lives inside GTM, which is version-controlled and reversible (one-click rollback to a prior container version). It avoids editing the live Head & Footer Code HEAD field, which carries the GTM loader and has no version history; a fat-finger error there takes down all tracking. The service_label field that a real data layer would provide is supplied by a GTM Lookup Table variable instead (§5.6). Method B remains documented as the alternative — choose it only if page context must reach a server-side system (e.g. BigQuery Stage 4, §13.8), which a GTM-only variable cannot do. The §6.2 dlv variables and §5.1 schema describe the Method B path; under Method A, tags reference {{JS - ...}} / {{LT - ...}} instead. Pick one — do not run both.

5.6 Method A — GTM JavaScript variables (full implementation)

Populates page context entirely inside GTM, with no change to WordPress.

Step 1 — create three Custom JavaScript Variables. GTM → Variables → New → Variable type: Custom JavaScript. Name them JS - page_type, JS - service_name, JS - location_name; paste the functions below (one per variable).

Step 2 — create the LT - service_label Lookup Table variable. GTM → Variables → New → Variable type: Lookup Table. Input variable = {{JS - service_name}}. Map each service slug to its label — community-accessCommunity Access, ndis-campsNDIS Camps, ndis-respiteNDIS Respite, supported-independent-livingSupported Independent Living, behaviour-support-plansBehaviour Support Plans. Default value (not set). This supplies the service_label field that a real data layer (Method B) would otherwise provide.

Step 3 — create JS - conversion_value — the variable in §6.4.

Step 4 — wire them into tags. Every GA4 tag references these as {{JS - page_type}}, {{LT - service_label}} etc., in place of the {{dlv - ...}} variables listed in §6.2. The GA4 page_context event tag (§6.3, tag #2) carries them to GA4.

Step 5 — validate in GTM Preview (§16.1) — visit one page of each type and confirm each variable resolves.

The three variable functions:

// GTM Custom JavaScript Variable — "JS - page_type"
// URL rules verified against a full-site crawl, 22 May 2026 (341 pages).
function() {
  var parts = window.location.pathname.split('/').filter(Boolean);
  if (parts.length === 0) return 'homepage';
  var seg0 = parts[0];

  if (seg0 === 'services')  return 'service';
  if (seg0 === 'locations') return 'location';
  if (seg0 === 'about')     return parts[1] === 'jobs' ? 'careers' : 'about';
  if (seg0 === 'pages') {
    var slug = parts[1];
    if (slug === 'get-support')    return 'enquiry';
    if (slug === 'event-waitlist') return 'event';
    if (slug === 'support-worker' || slug === 'support-coordinators') return 'careers';
    return 'info';
  }
  if (seg0 === 'event' || seg0 === 'events') return 'event';
  if (seg0 === 'venue' || seg0 === 'organiser') return 'other';
  if (seg0 === 'blog')      return 'blog';
  if (seg0 === 'resources') return 'resource';
  if (seg0 === 'partners')  return 'partner';
  if (seg0.indexOf('thank-you') === 0) return 'thankyou';
  if (seg0 === 'terms-of-use' || seg0 === 'privacy-policy' || seg0 === 'incident-reporting') return 'info';

  // Paid-ad landing pages — unlinked from site nav, reached only via ad clicks.
  // MAINTAIN THIS LIST: add a slug here whenever marketing launches a new landing page.
  var landingPages = [
    'explore-our-ndis-services', 'our-services-in-brisbane',
    'autism-support-with-what-ability', 'downs-syndrome-support-with',
    'how-to-use-funding', 'how-to-epilepsy-training'
  ];
  if (landingPages.indexOf(seg0) !== -1) return 'landing';

  return 'other';
}
// GTM Custom JavaScript Variable — "JS - service_name"
function() {
  var parts = window.location.pathname.split('/').filter(Boolean);
  if (parts[0] === 'services' && parts.length >= 2) return parts[1];
  return null;
}
// GTM Custom JavaScript Variable — "JS - location_name"
function() {
  var parts = window.location.pathname.split('/').filter(Boolean);
  if (parts[0] === 'locations' && parts.length >= 2) return parts[1];
  return null;
}

5.7 Method B — real data layer push (full implementation)

Method B pushes a real dataLayer object on every page before the GTM container loads. GTM then reads it via the {{dlv - ...}} variables in §6.2. The §5.1 schema and §6.2 dlv variables are written around this path; Method A (§5.6) is the recommended alternative — see §5.5.

Scope: Method B handles Layer 1 (page context) only. Layer 2 conversion events (§5.2) are pushed at the moment of action by the form integration (§13.1); Layer 3 first-touch (§5.3) is the separate cookie script in §13.6.

The WordPress environment audit (Appendix C) found: GTM is injected by the Head & Footer Code plugin (active); WPCode Lite is installed but inactive. That gives two routes — pick one.

Route B1 — Head & Footer Code plugin (recommended)

The site already injects GTM through the Head & Footer Code plugin. Add the data layer in the same place — no new plugin, no PHP, no activation step.

  1. WordPress Admin → Tools → Head & Footer Code.
  2. In the HEAD Code field, paste the script below above the existing GTM snippet (same field, ordered first).
  3. Save.
<!-- WhatAbility — Data Layer (Layer 1: Page Context) -->
<!-- Tools > Head & Footer Code > HEAD Code — paste ABOVE the GTM snippet -->
<script>
// URL rules verified against a full-site crawl, 22 May 2026 (341 pages).
(function () {
  var parts = window.location.pathname.split('/').filter(Boolean);
  var serviceLabels = {
    'community-access': 'Community Access',
    'ndis-camps': 'NDIS Camps',
    'ndis-respite': 'NDIS Respite',
    'supported-independent-living': 'Supported Independent Living',
    'behaviour-support-plans': 'Behaviour Support Plans'
  };
  var states = {
    'sydney': 'NSW', 'perth': 'WA', 'melbourne': 'VIC',
    'brisbane': 'QLD', 'gold-coast': 'QLD'
  };
  // Paid-ad landing pages — unlinked from nav. MAINTAIN when a new one launches.
  var landingPages = [
    'explore-our-ndis-services', 'our-services-in-brisbane',
    'autism-support-with-what-ability', 'downs-syndrome-support-with',
    'how-to-use-funding', 'how-to-epilepsy-training'
  ];

  var pageType = 'other';
  var serviceName = null, locationName = null;
  var seg0 = parts[0];

  if (parts.length === 0) {
    pageType = 'homepage';
  } else if (seg0 === 'services') {
    pageType = 'service';
    if (parts.length >= 2) serviceName = parts[1];
  } else if (seg0 === 'locations') {
    pageType = 'location';
    if (parts.length >= 2) locationName = parts[1];
  } else if (seg0 === 'about') {
    pageType = (parts[1] === 'jobs') ? 'careers' : 'about';
  } else if (seg0 === 'pages') {
    if (parts[1] === 'get-support') pageType = 'enquiry';
    else if (parts[1] === 'event-waitlist') pageType = 'event';
    else if (parts[1] === 'support-worker' || parts[1] === 'support-coordinators') pageType = 'careers';
    else pageType = 'info';
  } else if (seg0 === 'event' || seg0 === 'events') {
    pageType = 'event';
  } else if (seg0 === 'venue' || seg0 === 'organiser') {
    pageType = 'other';
  } else if (seg0 === 'blog') {
    pageType = 'blog';
  } else if (seg0 === 'resources') {
    pageType = 'resource';
  } else if (seg0 === 'partners') {
    pageType = 'partner';
  } else if (seg0.indexOf('thank-you') === 0) {
    pageType = 'thankyou';
  } else if (seg0 === 'terms-of-use' || seg0 === 'privacy-policy' || seg0 === 'incident-reporting') {
    pageType = 'info';
  } else if (landingPages.indexOf(seg0) !== -1) {
    pageType = 'landing';
  }

  window.dataLayer = window.dataLayer || [];
  window.dataLayer.push({
    page_type:      pageType,
    content_group:  pageType,
    service_name:   serviceName,
    service_label:  (serviceName && serviceLabels[serviceName]) ? serviceLabels[serviceName] : null,
    location_name:  locationName,
    location_state: (locationName && states[locationName]) ? states[locationName] : null,
    page_template:  pageType
  });
})();
</script>

Route B2 — WPCode PHP snippet (alternative)

A server-side equivalent. Requires activating WPCode Lite first (it is installed but inactive — Appendix C). Use this route only if a server-side push is specifically preferred.

Step 1 — activate WPCode. WordPress Admin → Plugins → activate "WPCode Lite".

Step 2 — add the snippet. WPCode → Add Snippet → Add Your Custom Code → PHP Snippet. Paste the code below. Set Location = Site Wide Header, Priority = 1 (must run before the GTM container). Save and Activate.

<?php
/**
 * WhatAbility — Data Layer (Layer 1: Page Context)
 * WPCode snippet — Location: Site Wide Header, Priority: 1
 * Outputs dataLayer.push() before the GTM container loads.
 */
add_action( 'wp_head', function () {

    // URL rules verified against a full-site crawl, 22 May 2026 (341 pages).
    $path  = trim( (string) parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ), '/' );
    $parts = ( $path === '' ) ? array() : explode( '/', $path );

    // service slug => human-readable label
    $services = array(
        'community-access'             => 'Community Access',
        'ndis-camps'                   => 'NDIS Camps',
        'ndis-respite'                 => 'NDIS Respite',
        'supported-independent-living' => 'Supported Independent Living',
        'behaviour-support-plans'      => 'Behaviour Support Plans',
    );
    // location slug => state
    $locations = array(
        'sydney' => 'NSW', 'perth' => 'WA', 'melbourne' => 'VIC',
        'brisbane' => 'QLD', 'gold-coast' => 'QLD',
    );
    // paid-ad landing pages — unlinked from nav. MAINTAIN when a new one launches.
    $landing_pages = array(
        'explore-our-ndis-services', 'our-services-in-brisbane',
        'autism-support-with-what-ability', 'downs-syndrome-support-with',
        'how-to-use-funding', 'how-to-epilepsy-training',
    );

    $page_type     = 'other';
    $service_name  = null;
    $location_name = null;
    $seg0          = isset( $parts[0] ) ? $parts[0] : '';
    $seg1          = isset( $parts[1] ) ? $parts[1] : '';

    if ( empty( $parts ) ) {
        $page_type = 'homepage';
    } elseif ( $seg0 === 'services' ) {
        $page_type = 'service';
        if ( $seg1 !== '' ) { $service_name = $seg1; }
    } elseif ( $seg0 === 'locations' ) {
        $page_type = 'location';
        if ( $seg1 !== '' ) { $location_name = $seg1; }
    } elseif ( $seg0 === 'about' ) {
        $page_type = ( $seg1 === 'jobs' ) ? 'careers' : 'about';
    } elseif ( $seg0 === 'pages' ) {
        if ( $seg1 === 'get-support' ) {
            $page_type = 'enquiry';
        } elseif ( $seg1 === 'event-waitlist' ) {
            $page_type = 'event';
        } elseif ( $seg1 === 'support-worker' || $seg1 === 'support-coordinators' ) {
            $page_type = 'careers';
        } else {
            $page_type = 'info';
        }
    } elseif ( $seg0 === 'event' || $seg0 === 'events' ) {
        $page_type = 'event';
    } elseif ( $seg0 === 'venue' || $seg0 === 'organiser' ) {
        $page_type = 'other';
    } elseif ( $seg0 === 'blog' ) {
        $page_type = 'blog';
    } elseif ( $seg0 === 'resources' ) {
        $page_type = 'resource';
    } elseif ( $seg0 === 'partners' ) {
        $page_type = 'partner';
    } elseif ( strpos( $seg0, 'thank-you' ) === 0 ) {
        $page_type = 'thankyou';
    } elseif ( in_array( $seg0, array( 'terms-of-use', 'privacy-policy', 'incident-reporting' ), true ) ) {
        $page_type = 'info';
    } elseif ( in_array( $seg0, $landing_pages, true ) ) {
        $page_type = 'landing';
    }

    $data = array(
        'page_type'      => $page_type,
        'content_group'  => $page_type,
        'service_name'   => $service_name,
        'service_label'  => ( $service_name && isset( $services[ $service_name ] ) )
                              ? $services[ $service_name ] : null,
        'location_name'  => $location_name,
        'location_state' => ( $location_name && isset( $locations[ $location_name ] ) )
                              ? $locations[ $location_name ] : null,
        'page_template'  => $page_type,
    );

    echo "\n<script>window.dataLayer=window.dataLayer||[];"
       . "dataLayer.push(" . wp_json_encode( $data ) . ");</script>\n";

}, 1 );

Step 3 — verify ordering. WPCode Priority 1 on wp_head runs early. View the page source (Ctrl+U) and confirm the dataLayer.push <script> appears above the GTM container snippet. If GTM still loads first, the Head & Footer Code plugin (Route B1) gives precise control of ordering and is the safer choice.

Validation (both routes)

After either route, validate in GTM Preview (§16.1): visit one page of each type and confirm every {{dlv - ...}} variable resolves to the expected value. View the page source and confirm the dataLayer.push runs before the GTM container.

URL dependency (Risk R4). Both routes (and Method A) derive context from the URL path. The rules above were verified against a full-site crawl (22 May 2026, 341 pages) and classify 100% of live pages. One ongoing risk: paid-ad landing pages are matched by an explicit slug list — a new landing page must be added to that list (landingPages / $landing_pages) or it falls back to page_type: other and loses page-type reporting. See Risk R4.

6. GTM Implementation

6.1 GTM is already installed — verify it

GTM container GTM-MCXFB7T is already live on the site, injected via the Head & Footer Code plugin — the HEAD and BODY snippets are both correctly in place (Appendix C). Do not reinstall it. Verify before building tags:

  1. GTM → Preview → enter the site URL → confirm the debug connection establishes.
  2. Confirm the container fires on every page type (homepage, service, location, blog).
  3. Confirm there is only one GTM container on the site — a second container would double-count.

All new work (variables, tags, triggers) goes into the existing GTM-MCXFB7T container. Before adding anything, work through the container remediation in Appendix B — the container carries dead Universal Analytics tags, no consent mode, and ~28 redundant per-state tags that must be cleaned up first.

6.2 Data Layer Variables in GTM

Create these Data Layer Variables (dlv) once. Every tag and trigger can then reference them.

GTM VariableData Layer keyDefault
dlv - page_typepage_typeunknown
dlv - content_groupcontent_group(not set)
dlv - service_nameservice_name(not set)
dlv - service_labelservice_label(not set)
dlv - location_namelocation_name(not set)
dlv - location_statelocation_state(not set)
dlv - service_interestservice_interest(not set)
dlv - location_interestlocation_interest(not set)
dlv - conversion_valueconversion_value0
dlv - event_nameevent_name(not set)
dlv - cta_locationcta_location(not set)

6.3 GTM tag reference

#TagTypeTrigger
1GA4 — ConfigurationGA4 ConfigAll Pages
2GA4 — Page ContextGA4 Event (page_context)All Pages
3GA4 — Phone Call ClickGA4 EventClick URL contains tel:1300
4GA4 — Enquiry SubmittedGA4 EventOnboardify completion (see Section 13)
5GA4 — Event SignupGA4 EventForm submission on /events/ pages
6GA4 — Support Worker AppliedGA4 EventForm submission on /careers/ pages
7GA4 — CTA ClickedGA4 EventClick Classes contains btn/cta/button
8GA4 — Scroll DepthGA4 EventScroll depth 50% / 75% / 90%
9Meta Pixel — BaseCustom HTMLAll Pages
10Meta Pixel — LeadCustom HTMLSame as tag 4
11Meta Pixel — ContactCustom HTMLSame as tag 3
12Meta Pixel — CompleteRegistrationCustom HTMLSame as tag 5
13Google Ads — RemarketingGoogle Ads RemarketingAll Pages

6.4 Conversion value variable

GTM calculates the dollar value automatically from the service — no per-page updates needed.

// GTM Custom JavaScript Variable — "JS - conversion_value"
function() {
  var service = {{JS - service_name}};
  var values = {
    'supported-independent-living': 400,
    'ndis-camps': 300,
    'ndis-respite': 280,
    'community-access': 250,
    'behaviour-support-plans': 200
  };
  return values[service] || 200;
}

6.5 Diagram — conversion tracking pathways

flowchart LR subgraph Triggers["Conversion triggers"] E1["Enquiry form
completed"] E2["tel: link
clicked"] E3["Event/camp form
submitted"] E4["Careers form
submitted"] end E1 --> GTM["GTM"] E2 --> GTM E3 --> GTM E4 --> GTM GTM --> GA4E["GA4 events:
enquiry_submitted
phone_call_clicked
event_signup
support_worker_applied"] GTM --> METAE["Meta events:
Lead / Contact /
CompleteRegistration"] GA4E -->|import: 3 of 4| GADS["Google Ads
conversions"] GA4E -.->|support_worker_applied
NOT imported| GADS

support_worker_applied is deliberately not imported into Google Ads — mixing recruitment conversions with client-acquisition conversions corrupts Smart Bidding.

7. Conversion Events

Four conversion events. Mark each as a conversion in GA4 (Admin → Events → "Mark as conversion"), then import the first three into Google Ads.

GA4 eventFires whenMeta eventGoogle AdsValue (AUD)
enquiry_submittedGet Support / contact form completed via OnboardifyLeadImport$200–$400 by service
phone_call_clickedClick on tel:1300358714ContactImport$150
event_signupCamp / event waitlist or registration submittedCompleteRegistrationImport$150
support_worker_appliedSupport worker / careers application submittedLeadDo not importNo value

7.1 Conversion values

Values drive Google Ads value-based bidding (tROAS). They are estimates — refine as real data accumulates.

ConversionValueRationale
enquiry_submitted — SIL$400Highest lifetime-value service
enquiry_submitted — NDIS Camps$300High-ticket, recurring
enquiry_submitted — NDIS Respite$280High-ticket, recurring
enquiry_submitted — Community Access$250Highest-volume entry service — anchor for tROAS target
enquiry_submitted — Behaviour Support$200Specialist, lower volume
phone_call_clicked$150High intent, unqualified
event_signup$150Entry to relationship; often converts to ongoing support
support_worker_appliedRecruitment, not revenue — tracked separately
The earlier Analytics Tracking document used a flat "$200" placeholder for enquiries. That is superseded by the per-service table above.

7.2 Event payloads

<!-- phone_call_clicked — GTM fires automatically on tel: click -->
<script>
  dataLayer.push({ 'event': 'phone_call_clicked', 'conversion_value': 150, 'currency': 'AUD' });
</script>

<!-- event_signup -->
<script>
  dataLayer.push({
    'event': 'event_signup',
    'event_name': 'Summer Camp Sydney 2026',
    'event_type': 'camp',                  // camp | workshop | community
    'event_location': 'sydney',
    'event_dates': '2026-01-15',
    'conversion_value': 150, 'currency': 'AUD'
  });
</script>

<!-- support_worker_applied — no value, kept separate from client conversions -->
<script>
  dataLayer.push({
    'event': 'support_worker_applied',
    'application_type': 'support-worker',  // support-worker | coordinator | other
    'location_interest': 'sydney'
  });
</script>

8. Custom Events (Engagement / Micro-conversions)

Non-conversion events that explain behaviour. Implemented via GTM — most need no developer.

EventTriggerKey properties
cta_clickedClick on a primary CTA buttonbutton_text, cta_location (hero/nav/footer/inline/sticky-bar), cta_destination
service_page_viewedPage view on /services/*service_name, location_name
location_page_viewedPage view on /locations/*location_name
blog_post_viewedPage view on /blog/*post_title, post_category
scroll_depthUser scrolls 50% / 75% / 90%percent_scrolled, page_type
outbound_link_clickedClick on external link (Onboardify, Supportmate, social)link_url, link_text

GA4 automatic events (no setup — confirm Enhanced Measurement is on): page_view, scroll (90%), click (outbound), session_start, first_visit.

<!-- cta_clicked -->
<script>
  dataLayer.push({
    'event': 'cta_clicked',
    'button_text': 'Get Support',          // {{Click Text}}
    'cta_location': 'hero',                // hero | nav | footer | inline | sticky-bar
    'cta_destination': 'enquiry-form'      // enquiry-form | phone | email | external
  });
</script>

9. Custom Dimensions

Create in GA4 Admin → Custom definitions. These unlock segmentation ("which location converts best", "which service page drives enquiries").

DimensionScopeParameterExample values
Page TypeEventpage_typehomepage, service, location, about, careers, enquiry, event, blog, resource, partner, info, landing, thankyou, other
Service NameEventservice_namecommunity-access, ndis-camps, ndis-respite, supported-independent-living, behaviour-support-plans
Location NameEventlocation_namesydney, perth, melbourne, brisbane, gold-coast
Service InterestEventservice_interest(same as Service Name — what the lead chose)
Location InterestEventlocation_interest(same as Location Name — what the lead chose)
CTA LocationEventcta_locationhero, nav, footer, inline, sticky-bar
First Touch SourceUserfirst_touch_sourcegoogle, facebook, newsletter, (direct)
First Touch MediumUserfirst_touch_mediumcpc, paid-social, email, organic
First Touch CampaignUserfirst_touch_campaigncommunity-access_sydney, etc.
service_name vs service_interestservice_name is the page the user was on; use it for content-performance analysis. service_interest is what the lead chose in the form (or page context at submission); use it for lead-quality analysis.

10. Meta Ads Tracking

10.1 Meta Pixel events

Meta eventFires whenMaps to GA4 event
PageViewEvery page loadpage_view
LeadEnquiry form submittedenquiry_submitted
ContactPhone number clickedphone_call_clicked
CompleteRegistrationEvent / camp signup submittedevent_signup
LeadSupport worker applicationsupport_worker_applied
ViewContentService or location page viewedservice_page_viewed

10.2 Conversions API (CAPI)

The browser-only pixel loses roughly 30% of conversions to iOS 14+ and ad-blockers. Enable the Conversions API in Meta Events Manager (partner integration or server-side) for accurate attribution. This requires developer involvement and is a Phase 2+ task.

<!-- Meta Pixel Lead — Custom HTML tag, fires with enquiry_submitted -->
<script>
  fbq('track', 'Lead', {
    content_name: 'NDIS Enquiry',
    content_category: 'Get Support'
  });
</script>

11. Google Ads Tracking

11.1 Conversion import

Link the GA4 property to Google Ads (GA4 Admin → Google Ads Links), then import conversions (Google Ads → Tools → Conversions → Import → Google Analytics 4). Use GA4-linked conversions, not native Google Ads tags — avoids duplicate counting and gives unified attribution.

Conversion actionSourceCountingValue
Enquiry SubmittedImport — enquiry_submittedOne per conversionPer-service (Section 7.1)
Phone Call ClickedImport — phone_call_clickedOne per conversion$150
Event SignupImport — event_signupOne per conversion$150

11.2 Attribution settings

SettingRecommendedWhy
Attribution modelData-drivenGoogle ML across all touchpoints
Conversion window — click30 daysNDIS has a long consideration cycle; the 7-day default misses conversions
Conversion window — view1 dayView-through is rarely the primary driver here
CountingOne per conversionPrevents double-counting repeat submissions
Included in "Conversions"enquiry_submitted, phone_call_clicked, event_signup onlyKeep support_worker_applied out — it corrupts client-acquisition bidding

11.3 Smart Bidding rollout

Smart Bidding quality depends entirely on the conversion signal it is fed. Roll out in stages.

PhaseTimeframeBidding strategyNotes
LearningWeeks 1–6Maximise Conversions (enquiry signal only)Expect higher CPC and volatility while it learns
TransitionMonth 2–3Maximise Conversions + start uploading confirmed-client dataAdd confirmed client as a separate conversion action; keep enquiry as secondary
tROASMonth 4+ (30+ confirmed clients/month)Target ROAS using confirmed-client valueStart target at 150–200%, tighten as volume grows
Do not switch to tROAS early. Smart Bidding needs 30+ conversions per month per campaign. Switching too early over-constrains delivery and ads stop showing.

12. UTM Naming Conventions

Enforce lowercase + underscores/hyphens. Maintain a shared UTM spreadsheet so every campaign link is documented.

Channelutm_sourceutm_mediumutm_campaign patternExample
Google Search Adsgooglecpc{service}_{location}community-access_sydney
Google Display Adsgoogledisplay{service}_{audience}respite_parents
Facebook Adsfacebookpaid-social{service}_{location}ndis-camps_perth
Instagram Adsinstagrampaid-social{service}_{location}community-access_melbourne
Email newsletternewsletteremail{month}_{topic}may_camps-open
Organic Facebookfacebooksocial{post-topic}camp-highlights
Partner / referral{partner-name}referral{placement}footer-link

utm_content — use to split-test creatives within a campaign: video_testimonial, static_image_family, carousel_activities.

The {service} and {location} tokens must match the data layer slugs exactly (Section 5.1) so campaign data joins cleanly to on-site behaviour.

13. CRM / Onboardify Integration & Loop Closure

This is the hardest and highest-value part. "Closing the loop" means being able to say: "The Perth Camps campaign spent $3,200, produced 12 enquiries, 8 of which became confirmed clients — $400 per client."

13.1 Onboardify form tracking — three scenarios

Enquiry forms run through Onboardify (a third-party tool). How to track the conversion depends on how Onboardify is embedded. First action: submit a test enquiry on the live site and watch the URL bar.

ScenarioHow to detectTracking approach
A — Thank-you redirectAfter submit, user lands on a whatability.com.au/thank-you/ pageGTM Page View trigger on the thank-you URL fires enquiry_submitted. No developer needed.
B — Embedded iframeForm is on the page; URL does not change on submitGTM cannot see inside iframes. Ask Onboardify support if they fire a postMessage on completion; if yes, a small JavaScript snippet listens for it and pushes enquiry_submitted. Add it via the Head & Footer Code plugin (already active) or by activating WPCode Lite. Developer needed.
C — Stays on Onboardify domainUser completes the form on onboardify.au and never returnsRequires an Onboardify webhook → server-side GA4 Measurement Protocol event. Developer needed.
Resolved — live test, 22 May 2026. A GCLID implementation test on the live "Get Support" page confirmed the Onboardify form is an embedded iframe (id="onboardify_iframe") loading portal.onboardify.au/client_form/.../2/112 — a Scenario B/C hybrid: the form markup lives on the Onboardify domain inside an iframe, so GTM cannot read its submit event. Conversion tracking (enquiry_submitted) therefore still needs either an Onboardify completion postMessage (Scenario B) or a webhook (Scenario C). GCLID passthrough into this iframe is already built and working — see §13.8.
// Scenario B — add via Head & Footer Code plugin (HEAD or FOOTER field)
// Confirm the exact message format with Onboardify support first.
window.addEventListener('message', function(event) {
  if (event.origin.indexOf('onboardify') === -1) return;
  if (event.data && event.data.type === 'form_complete') {
    window.dataLayer = window.dataLayer || [];
    dataLayer.push({
      'event': 'enquiry_submitted',
      'form_type': 'get-support',
      'service_interest': event.data.service || null,
      'location_interest': event.data.location || null
    });
  }
});

13.2 Diagram — CRM & tracking integrations

flowchart LR V["Visitor + Data Layer
captures gclid / fbclid /
GA client ID / page context"] --> ONB["Onboardify
enquiry form"] ONB -->|"hidden fields carry
IDs + UTM"| ONBREC["Onboardify
lead record"] ONBREC -->|"intake — native /
Zapier / manual?"| SM["Supportmate
participant record"] SM -->|"confirmed milestone
+ lead source"| RPT["Reporting
cost per client"] SM -.->|"Option B: webhook"| GA4["GA4"] SM -.->|"Option C: monthly CSV"| ADS["Google Ads + Meta
offline conversions"] classDef gap fill:#fdecec,stroke:#dc2626,color:#7f1d1d class ONBREC,SM gap

Red nodes are where attribution currently breaks — see open questions Q1–Q9 (Section 18).

13.3 The three loop-closure mechanisms

These connect a confirmed client back to the original ad click. Listed simplest to most powerful — they stack; start with one, upgrade later.

MechanismCommon IDPrecisionEffortWhat it gives you
1 — Enquiry IDOnboardify reference (ENQ-00123)Campaign-level (manual cross-reference)Lowest — needs Onboardify to expose the refEnquiry-to-client rate by service & state
2 — GCLID / FBCLIDGoogle/Meta click IDPer-campaign, per-keywordMedium — capture click ID, store in CRM, monthly CSV uploadTrue per-ad attribution; Smart Bidding learns which clicks become clients
3 — GA4 Client IDGA4 cookie IDFull session/campaign historyHighest — server-to-server API call from CRM to GA4Confirmed-client events appear in GA4 automatically, in real time

13.4 The three implementation options

OptionMechanismDeveloperOngoing effortData quality
A — Internal "Client Confirmed" pageTeam submits an internal form (/internal/client-confirmed/) per confirmed client; fires a GA4 event1–2 daysManual, per clientDepends on team discipline — good enough to start
B — Onboardify/CRM webhook → GA4Lead status change to "confirmed" auto-fires a signal to GA43–5 daysNone — automaticHigh — as accurate as CRM status updates
C — Offline conversion importMonthly CSV of confirmed clients matched by GCLID/FBCLID, uploaded to Google Ads + Meta~1 week (click-ID capture + CRM field)~30 min/monthHighest — tells ad platforms directly who converted

Minimum viable version (before any option is built): the team keeps a simple spreadsheet — one row per confirmed client, columns for service and state. At month-end, compare against GA4 enquiry counts. No per-campaign precision, but it answers the most important question: which services and states produce actual clients.

13.5 Diagram — attribution flow (loop closed)

sequenceDiagram participant U as User participant W as Website + Data Layer participant GA as GA4 participant ADS as Google Ads / Meta participant CRM as Onboardify → Supportmate U->>W: Clicks ad (UTM + gclid/fbclid) W->>W: Data layer captures click ID + page context U->>W: Submits enquiry W->>GA: enquiry_submitted (service, location, value) W->>ADS: Conversion signal W->>CRM: Lead record + click ID (hidden fields) Note over CRM: Team qualifies & onboards CRM->>CRM: Mark "confirmed client" + milestone date CRM-->>ADS: Offline conversion import (matched by click ID) CRM-->>GA: Confirmed-client signal (webhook / API) Note over ADS: Smart Bidding now optimises
toward confirmed clients, not just enquiries

13.6 First-touch attribution

GA4 natively stores only the last session's UTMs. To answer "which campaigns introduce leads", capture first-touch UTMs in a cookie:

// On page load — store first UTM in a 90-day cookie, push to data layer every visit
const params = new URLSearchParams(window.location.search);
const ftSource = params.get('utm_source');
if (ftSource && !getCookie('ft_source')) {
  setCookie('ft_source',   ftSource,                90);
  setCookie('ft_medium',   params.get('utm_medium'),   90);
  setCookie('ft_campaign', params.get('utm_campaign'), 90);
}
window.dataLayer = window.dataLayer || [];
dataLayer.push({
  'first_touch_source':   getCookie('ft_source')   || '(direct)',
  'first_touch_medium':   getCookie('ft_medium')   || '(none)',
  'first_touch_campaign': getCookie('ft_campaign') || '(not set)'
});

Register the three first_touch_* fields as User-scoped custom dimensions (Section 9).

13.7 Supportmate

For the loop to close, Supportmate must hold two things it likely does not today:

  1. A confirmed-client milestone with a date — service agreement signed, or first booking confirmed. The exact definition must be agreed (see Q6).
  2. A lead-source field — channel/campaign carried in from Onboardify at intake.

13.8 GCLID attribution pipeline — live test status (22 May 2026)

A GCLID implementation test on the live "Get Support" page (onboardify-gclid-tracking-test.md, two runs — direct landing, and homepage → "Get Support" nav journey) verified the GCLID attribution pipeline already on the site. This is the mechanism-2 path (§13.3) that feeds Option C (§13.4) — and the front half is already built and working.

Pipeline stages

StageWhat it doesStatus
1.1 — Capture & persistGCLID read from the landing URL, written to a 1st-party cookie; survives cross-page navigation✅ Working — cookies gclid and gclid_cookie both set; both survive a homepage → form journey
1.2 — Iframe injectionGCLID appended to the Onboardify iframe src✅ Working — ?gclid=... present on the portal.onboardify.au iframe src in both runs
3 — Hidden field captureGCLID lands in a hidden <input id="gclid"> inside the Onboardify form and persists across all 3 steps✅ Working — value correct at pre-submit
4 — BigQuery validationSubmitted GCLID validated / processed in BigQuery⬜ Not yet tested — needs a real or staging submission
5 — Measurement ProtocolValidated leads pushed back to GA4 via the Measurement Protocol⬜ Not yet tested

Verdict: GCLID capture → cookie persistence → iframe injection → hidden-field handoff works end-to-end across both direct and cross-page journeys. Three gaps:

GapSeverityDetail & action
Cookie naming diverges from specLowThe pipeline spec called for one tracking_params cookie (JSON of gclid + UTMs). The live site uses two cookies — gclid and gclid_cookie — each holding only the gclid value. Functional, but any downstream code (BigQuery SQL, other GTM tags) referencing tracking_params finds nothing. Resolve by aligning the spec/downstream references to the live cookie names, or renaming the cookies. Ties to Appendix B-9.
UTM parameters not passedMediumutm_source and utm_medium were present in the landing URL but were not stored in any cookie and not appended to the iframe src. Channel attribution is lost at the form layer. If Stage 4 BigQuery logic filters or qualifies leads by UTM, valid leads will fail on null values. Fix: extend the GTM injection tag to read utm_source/utm_medium from the URL, append them to the iframe src, and persist them to the tracking cookie.
No custom GTM eventsLowOnly standard lifecycle events (gtm.js, gtm.dom, gtm.load) fire — no gclid_iframe_injected or similar. Add a dataLayer.push({ event: 'gclid_iframe_injected', gclid: gclidValue }) when the injection script runs, for observability in GTM Preview / GA4 DebugView.

Next: test Stage 4 (BigQuery) and Stage 5 (Measurement Protocol) after a real or staging submission; fix UTM passthrough before any UTM-based lead qualification goes live.

14. Call Tracking / 1300 Number Handling

The site uses a single national number — 1300 358 714 — on every page. Two distinct tracking needs:

14.1 On-site phone-click tracking (Phase 1 — easy)

GTM fires phone_call_clicked when a visitor clicks the tel: link. This captures the click with full page context (service, location, page type) — but not whether the call connected or who called. No developer needed.

14.2 True call attribution (later — needs a decision)

A single 1300 number cannot tell a Perth call from a Brisbane call, or a Google call from a Meta call. To attribute calls properly, Dynamic Number Insertion (DNI) is required: a tracking tool swaps the displayed number based on the visitor's source.

  • DNI providers (AU): Delacon, Invoca; CallRail is a common reference but check AU availability.
  • This is a budget + procurement decision — see Q5.

14.3 Diagram — call tracking & 1300 routing

flowchart TD subgraph Now["Phase 1 — on-site click tracking"] C1["Visitor clicks
tel:1300358714"] --> C2["GTM fires
phone_call_clicked"] C2 --> C3["GA4 event +
Meta Contact event"] C3 --> C4["Captured: page, service,
location, click time"] end subgraph Later["Phase 5 — DNI call attribution"] D1["Visitor arrives
with UTM source"] --> D2["DNI tool swaps
displayed number"] D2 --> D3["Visitor calls
source-specific number"] D3 --> D4["Call connected +
attributed to channel/location"] end

15. Reporting Requirements

15.1 Questions answerable once tracking is live

QuestionHow
Which state generates the most enquiries?GA4 report segmented by location_name
Which service drives the most enquiries?GA4 by service_interest on enquiry_submitted
Which campaign drove which enquiries?Session campaign × service_interest
Phone vs form — by state?phone_call_clicked vs enquiry_submitted by location_name
Which service pages engage vs bounce?scroll_depth by service_name + location_name
Which CTA placement converts?cta_location on cta_clicked
Which specific camp drove signups?event_name on event_signup

15.2 Questions answerable only after loop closure

QuestionNeeds
Of all enquiries, how many became clients?Confirmed-client signal (Section 13)
Cost per confirmed client by campaign?Loop closure + ad spend join
Which states have the best lead quality?Confirmed-client signal by location
Which service has the highest confirmation rate?Confirmed-client signal by service

15.3 Core GA4 explorations to build

  • Service × Location matrix — Free Form. Rows service_interest, columns location_interest, metric enquiry_submitted count. (Uses the form-side _interest dimensions, not page context — the site has no combined service-location pages, so on-page service_name/location_name never co-occur. See §5.1.)*
  • Primary conversion funnel — Funnel Exploration: Arrival → Service Interest (page_type = service/service-location) → Intent Signal (cta_clicked or scroll_depth ≥ 75%) → Conversion.
  • First-touch campaign report — rows first_touch_campaign, metric enquiry_submitted.
  • Phone vs form by service — dimensions service_name × event_name, filtered to the two conversion events.

15.4 Remarketing audiences

Build in GA4; they populate Google Ads and Meta automatically once linked.

AudienceGA4 conditionMembershipUse
All Site Visitorspage_view occurred30 daysDisplay / brand awareness
Service Page Visitorspage_type in (service, service-location)14 daysSearch RLSA, Meta retargeting
SIL Researchersservice_name = supported-independent-living30 daysRLSA bid +30%, Meta Lead Ads
NDIS Camps Researchersservice_name = ndis-camps21 daysCamps campaign RLSA / carousel
Sydney Visitorslocation_name = sydney14 daysSydney geo-retargeting
Converters — Suppressenquiry_submitted occurred90 daysExclude from acquisition campaigns
Job Seekers — Suppresspage_type = careers30 daysExclude from client-acquisition campaigns
Audience minimums — Google Ads needs 1,000 active users, Meta needs 100. Service-specific segments may take 4–8 weeks to reach minimum size.

16. QA / Testing Checklist

Test everything in GTM Preview and GA4 DebugView before publishing.

16.1 Layer 1 — page context

  • dataLayer push fires before the GTM snippet (Variables tab shows values on load)
  • service_name correct on /services/community-access/community-access
  • service_name + location_name both set on /services/ndis-camps/perth/
  • service_name = null/not-set on the homepage
  • page_type correct on one page of each template type

16.2 Layer 2 — conversion events

  • enquiry_submitted fires exactly once per submission (not twice)
  • conversion_value populates (not 0 / undefined) on enquiry_submitted
  • service_interest carries the correct value (test from a known service page)
  • phone_call_clicked fires on tel: click with page_type + service_name populated
  • ☐ Meta Lead event fires alongside enquiry_submitted (verify with Meta Pixel Helper)

16.3 Platform wiring

  • ☐ GA4 receiving data in Realtime
  • ☐ All 4 conversion events marked as conversions in GA4
  • ☐ GA4 linked to Google Ads; 3 conversions imported (support_worker_applied excluded)
  • support_worker_applied confirmed not in Google Ads primary conversions
  • ☐ Custom dimensions registered in GA4 (page_type, service_name, location_name, etc.)
Do not enable tROAS bidding until conversion_value is validated in GA4 DebugView. A value of 0/undefined makes Smart Bidding treat all conversions as equal and wastes budget.

17. Implementation Priorities & Rollout Order

Merged from the three source documents into one master sequence.

flowchart LR P0["Phase 0
Foundation
Week 1"] --> P1["Phase 1
Context + Core Conversions
Weeks 2-3"] P1 --> P2["Phase 2
Enrichment
Weeks 4-6"] P2 --> P3["Phase 3
Loop Closure I
Month 2"] P3 --> P4["Phase 4
Loop Closure II
Month 3+"] P2 --> P5["Phase 5
CMS Migration + DNI
When dev available"]

Phase 0 — Foundation (Week 1)

Depends on: nothing. Everything else depends on this.

  1. Clean up the existing GA4 property — work through Appendix A (rogue data stream, PII capture, disabled Enhanced Measurement). This is Risk R1.
  2. Clean up the existing GTM container — work through Appendix B remediation (dead UA tags, no consent mode, per-state tag sprawl, GCLID mess). Verify GTM-MCXFB7T fires correctly (§6.1); do not reinstall. Review the WordPress plugin environment in Appendix C.
  3. Create the GA4 Configuration tag → All Pages; verify in Realtime.
  4. Add the Meta Pixel base code in GTM; verify with Pixel Helper.
  5. Agree the UTM convention; create the shared UTM spreadsheet (Section 12).

Phase 1 — Page context & core conversions (Weeks 2–3)

Depends on: Phase 0.

  1. Implement page context Layer 1 via Method A — §5.6 GTM JavaScript variables (no WordPress change; GTM-versioned and reversible). Method B (§5.7) is the alternative if page context must reach a server-side system later.
  2. Create GA4 custom dimensions (page_type, service_name, location_name, …).
  3. Phone-call click tracking (phone_call_clicked).
  4. Resolve Onboardify conversion capture — the form is a confirmed embedded iframe (§13.1); confirm Scenario B (postMessage) vs C (webhook) with Onboardify, then implement enquiry_submitted.
  5. Event signup + support worker application tracking.
  6. Mark all 4 conversions in GA4; link GA4 → Google Ads; import 3 conversions.
  7. Add Meta conversion events (Lead, Contact, CompleteRegistration).

Phase 2 — Enrichment (Weeks 4–6)

Depends on: Phase 1.

  1. CTA click tracking, scroll depth, outbound link tracking.
  2. Per-service conversion values (JS - conversion_value).
  3. Build remarketing audiences and core GA4 explorations.
  4. Meta Conversions API (CAPI).

Phase 3 — Loop closure I (Month 2)

Depends on: Phase 1; answers to Q1–Q9.

  1. Define the confirmed-client milestone (Q6).
  2. Implement Option A (internal client-confirmed page) or a Supportmate lead-source field — whichever fits the process.
  3. Begin the minimum-viable spreadsheet: confirmed clients by service + state.

Phase 4 — Loop closure II (Month 3+)

Depends on: Phase 3; 2–3 months of enquiry data.

  1. Carry GCLID/FBCLID from Onboardify into Supportmate. (GCLID capture into the Onboardify form is already live — confirmed 22 May 2026, §13.8; the remaining work is the Onboardify→Supportmate handoff. FBCLID was not tested — verify separately.)
  2. Option C — monthly offline conversion import to Google Ads + Meta.
  3. Transition Google Ads to tROAS once 30+ confirmed clients/month (Section 11.3).

Phase 5 — First-touch attribution & call DNI (when time allows)

Depends on: Phase 2.

  1. Implement first-touch attribution (Section 13.6).
  2. Decide on and implement phone DNI for true call attribution (Q8).

(Page context Layer 1 is implemented via Method A in Phase 1 — no migration step needed.)

18. Open Questions, Assumptions & Risks

18.1 Risks

IDRiskImpactAction
R1The four planning documents assume a greenfield build, but the site already has a live GA4 property (250532328, configured via MonsterInsights) and GTM container (GTM-MCXFB7T, injected via the Head & Footer Code plugin) — carrying a rogue laletours.com data stream, PII (email_address) capture, disabled Enhanced Measurement, and a thin 3-variable data layer.High — building "from scratch" on top of a messy live setup creates duplicate tracking and data conflictsDo not create a new property. Clean the existing setup first — Appendices A, B, C. This is Phase 0.
R2Onboardify form is an embedded iframe on the onboardify.au domain (confirmed 22 May 2026) — GTM cannot see its submit eventMedium — enquiry_submitted needs an Onboardify completion postMessage or a webhookConfirm with Onboardify whether they emit a completion postMessage (Scenario B) or support a webhook (Scenario C). §13.1, §13.8
R3Conversion values are estimatesMedium — wrong values mislead tROAS biddingTreat as provisional; refine once 2–3 months of client data exists
R4Page context is derived from the URL path. A full-site crawl (22 May 2026, 341 pages — whatability_page_inventory.csv) verified the §5.6/§5.7 parsers against every live page: 100% classified, zero misses. Residual risk: paid-ad landing pages are unlinked from site nav and matched by a hardcoded slug list in the parser; a new landing page not added to that list falls back to page_type: other and loses page-type reporting.LowWhen marketing launches a new paid landing page, add its slug to the landingPages / $landing_pages list (§5.6 / §5.7). Make this a step in the campaign-launch checklist.
R5No consent mode. GTM container has zero consent gating (Appendix B-1) and Microsoft Clarity session recording is active (Appendix C) — recordings can capture PII with no consent gateHigh — compliance exposure under AU Privacy ActImplement Consent Mode v2; bring Clarity into the consent scope
R6Source documents claimed "no heatmapping/session recording installed" — the WordPress audit found Microsoft Clarity activeLow — but the planning docs were wrong on current stateTreat Clarity as live; include it in consent and privacy-policy scope
R7Intake form (Onboardify Get Support) collects sensitive disability-related information without a visible privacy acknowledgement, collection notice, explicit sensitive-information consent, guardian/carer authority confirmation, or marketing opt-inHigh — Privacy Act 1988 (Cth) compliance failure; disability data is sensitive information requiring explicit consent under APPs 1, 3, 5 and 6Add mandatory consent checkboxes and collection notice to the form before next intake campaign. See §19.

18.2 Open questions — must be answered by the business

Onboardify

  • Q1. What fields does the Onboardify form collect — does it ask service and location?
  • Q2. Does Onboardify capture the referring page URL automatically?
  • Q3. Does Onboardify support hidden fields (to carry UTM / click IDs)?
  • Q4. Does Onboardify support webhooks or an API? Does it redirect to a thank-you URL?

Supportmate

  • Q5. How does Onboardify connect to Supportmate — native, Zapier, manual, CSV?
  • Q6. Does Supportmate record a confirmed-client milestone date (agreement signed / first booking)? What is the agreed definition of "confirmed client"?
  • Q7. Can a custom "Lead Source" field be added to Supportmate participant records?

Phone / 1300

  • Q8. Who is the 1300 provider and what call reporting do they offer? Is there budget for DNI?

Lead process

  • Q9. Average time from enquiry to first booking? (Sets the ad attribution window.) Rough enquiry-to-client conversion rate and main drop-off reasons?

18.3 Assumptions

  • The site is WordPress with Elementor/Elementor Pro (confirmed — Appendix C) — Method B (§5.7) and the in-page snippets assume this.
  • URLs follow the structured pattern /services/{service}/{location}/both Method A's JS variables (§5.6) and the Method B snippet (§5.7) depend on this. The GCLID test (22 May 2026) already found this broken — the live "Get Support" page is at /pages/get-support/. See Risk R4.
  • Five services and five locations as listed in Sections 5 and 9; new ones inherit tracking automatically once Method B is live.
  • Google Ads campaigns are already running — meaning spend is currently happening with no conversion data, making Phases 0–1 urgent.

Appendix A — Existing GA4 / GTM Setup: Audit & Clean-up

This appendix folds in the WhatAbility-GA4-Audit-Checklist and data-layer-briefing audits. It documents what is already live on the site and what must be fixed before the rollout in Section 17. Phase 0, step 1 = work through this appendix.

A.1 What exists today

ItemDetail
GA4 property250532328 (Account 173068783)
GA4 measurement IDG-GDJSG62EF9 (confirmed in the GTM container export — Appendix B)
GA4 configured viaMonsterInsights WordPress plugin
GTM containerGTM-MCXFB7T — injected via the Head & Footer Code plugin (Appendix C)
Data layerInitialised on all pages but thin — only event, formId, response (see §5.0)
Standard events firingPage views, form submissions, button clicks, scroll, video, downloads, errors
Not configuredOutbound-link click tracking; any custom events; page/user/business context
The audit tooling labelled the site's "Get Support" CTAs as "donation buttons" — WhatAbility is an NDIS provider, not a donations org. Read "donation tracking" findings as support-CTA tracking (covered by cta_clicked / support_cta_click in §8).

A.2 GA4 clean-up — remediation list

Grouped by priority. Complete the Critical and Warning items before Phase 1.

🔴 Critical

ItemAction
Rogue laletours.com data streamA second stream (MonsterInsights - laletours.com, Stream ID 8176470249, MID G-JVH1196BE4) from an unrelated travel site is in this property. Delete it: Admin → Data Streams → select → Delete.
PII capture — email_address dimensionMonsterInsights captures email_address at event scope — likely breaches Google ToS and the Australian Privacy Act. Review triggering events; remove or hash.
Data redaction disabledEnable Email redaction and URL query-parameter redaction: Admin → Data Streams → What Ability → Redact data.

🟠 Warning

ItemAction
Enhanced Measurement OFFOnly Page Views active — scrolls, outbound clicks, site search, video, file downloads not captured. Audit MonsterInsights overlap, then re-enable.
Internal Traffic filter stuck in "Testing"Staff visits inflate counts. Admin → Data Filters → Internal Traffic → set to Active (confirm IP ranges first).
Broken key eventsajax_form_submit, purchase, work_with_us all show "No stream data". Fix or remove as key events.
Google Tag "Needs attention" — 2 issuesAdmin → Data Streams → Configure tag settings → resolve.
"Unassigned" channel spike (+1,605%)Traffic arriving without UTMs / unmatched source-medium. Audit campaign URLs; enforce the UTM convention (§12).
Consent Mode v2 not implementedAll four consent signals inactive. Integrate the cookie banner / CMP with the GA tag (also Risk R5).
Historical laletours.com contaminationCheck whether the rogue stream previously sent data into this property; assess affected reports.

🟡 Info / housekeeping

ItemAction
Currency set to USDChange to AUD: Admin → Property Details → Currency.
Business Size / Objectives blankComplete in Property Details.
17 custom dimensions, many legacyReview gclid, wp_user_id, legacy event_category / event_label, redundant page_path, undocumented action; add descriptions to all MonsterInsights dimensions.
gclid_capture event (~4.6k/week)Manual Google Ads attribution workaround — assess whether still needed now Google Ads is linked.
72 custom reports, many test/diagnosticAudit and archive ("test", "QLD check July", "gclid test", etc.).
Unnamed audiences / collectionsName or delete "Untitled segment", "Untitled collection", "What Ability Custom Reports"; consolidate duplicate Search Console collection.
Personalised Advertising enabledConfirm appropriate for an NDIS/disability org under policy and regulation.
No annotations ever createdStart annotating significant tracking changes.

✅ Already correct — no action

Data retention at 14 months (max); Search Console linked; attribution model is Data-driven.

A.3 Reconciliation with this document

Source planning docs assumedExisting setup realityResolution (applied in this document)
"Create a GA4 property"Property 250532328 already existsKeep it; clean per A.2. Do not create a new one.
"Install GTM"GTM-MCXFB7T already installedVerify the install only (§6.1); build new tags/variables into the existing container.
Data layer 3-layer schema (§5)Thin 3-variable data layer existsThe §5 schema replaces the thin one — additive, not a rebuild.
Custom events to "implement" (§8)Generic GTM events already fireDefine structured custom events; retire/ignore the generic ones where they overlap.

Appendix B — GTM Container Audit & Remediation

Audit of container export GTM-MCXFB7T_workspace40.json (workspace 40, exported 21 May 2026). The container holds 39 tags, 34 triggers, 25 variables, 3 folders. GA4 measurement ID confirmed as G-GDJSG62EF9. Every finding below has a solution. Complete the Critical and High items as part of Phase 0 (Section 17) before building new tracking.

B.1 Critical

B-1 — No consent mode. All 39 tags have consentStatus: NOT_SET. Solution: Implement GTM Consent Mode v2. Install a CMP (Complianz or Cookiebot for WordPress). Add a Consent Initialisation trigger. Set each tag's consent requirements: GA4 tags require analytics_storage; Google Ads tags require ad_storage + ad_user_data + ad_personalization. Until the CMP is live, set GA4/Ads tags to "require additional consent" so they hold by default.

B-2 — Dead Universal Analytics tags. Tags #13, #14 (ua type) send to UA-173068783-1 — a property Google shut down on 1 July 2024. Solution: Delete tags #13 and #14, the GA UA-173068783-1 settings variable, and triggers #10/#11 if unused elsewhere. No replacement needed — GA4 form events (#158, #160, #182) already cover these actions.

B-3 — PII to Google Ads with no consent gate. awec variables ("User data - Work with Us", "User data - Need support") capture name, email, phone, address and feed Enhanced Conversions on tags #16/#21. Solution: Keep Enhanced Conversions (it is legitimate and valuable) but (a) gate tags #16/#21 behind ad_user_data + ad_storage consent (B-1), (b) confirm the awec variable hashes fields before send — GTM's user-provided-data variable hashes automatically; verify it is configured, not raw, (c) document the practice in the privacy policy.

B.2 High — structural

B-4 — Per-state tag sprawl (~28 tags doing the work of ~4). "Need Support" and "Work with Us" each have 6 per-state tags; plus 8 per-state "Click" tags. Solution: Consolidate. Create one need_support GA4 tag and one work_with_us GA4 tag, each passing a state event parameter. Derive the state from one variable — a RegEx Table or Lookup Table variable that reads the thank-you URL slug. Replace the 8 state "Click" tags with one state_click tag + state parameter. Result: ~28 tags → ~4. Every future state then needs zero new tags.

B-5 — Conversion double-counting. Trigger #157 (Page URL CONTAINS thank-you-work-with-us) also matches thank-you-work-with-us-nsw etc., so both work_with_us and work_with_us_nsw fire on the same page. Same bug on Need Support (#159). Solution: Fixed automatically by B-4 (one tag, one fire). If B-4 is deferred: anchor the generic triggers with RegEx thank-you-work-with-us/?$ so they do not match state suffixes, or add the state pages as a blocking trigger.

B-6 — Duplicate Conversion Linker. Tags #15 and #115 (_import_2) are identical. Solution: Delete #115. Keep one Conversion Linker on All Pages.

B-7 — Broken ajax_form_submit path. Trigger #181 → tag #182 depends on the site pushing an ajax_form_submit dataLayer event; the GA4 audit confirmed this event receives no data. Solution: Resolve the Onboardify tracking scenario (Section 13.1 — A/B/C). If Onboardify can emit a completion event, wire #182 to it; otherwise delete trigger #181 + tag #182 and implement enquiry_submitted per Section 13.1.

B-8 — Dead CF7 need-support path + duplicate listeners. Nothing pushes CF7needsupportSubmitted, so trigger #11 / tag #13 never fire. Tags #9 and #116 are duplicate CF7 listeners pushing different event names. Solution: Keep #116 ("CF7 - All Page") as the single CF7 listener. Delete tag #9 and triggers #10/#11. Route conversions off the cf7submission event, distinguishing forms by {{dlv- formId}}.

B.3 Medium

B-9 — Two competing GCLID cookie mechanisms. #178 writes cookie gclid (90d) + localStorage; #186 writes gclid_cookie (30d). #185 reads gclid_cookie; #187 reads the URL only. Solution: Keep #186 as the single mechanism; extend its cookie to 90 days to match the Google Ads click window. Delete #178 (also has an uncustomised '.yourdomain.com' placeholder). Fix #187 to read the gclid_cookie cookie as a fallback when the URL has no gclid. Confirmed live (22 May 2026): a GCLID test observed both cookies — gclid and gclid_cookie — set and carrying the correct value, and both surviving a homepage → form navigation. The duplication is live and real; this consolidation still applies. See §13.8.

B-10 — gclid_capture fires on every page. Tag #183 sends a GA4 event on All Pages (~4,600/week of noise). Solution: Add a trigger condition so it fires only when {{URL - GCLID}} is set (not undefined/empty). It should fire on GCLID arrival, not every pageview.

B-11 — Fragile state Click triggers. Triggers #149–152, #165–172 match Click Text EQUALS "QLD" etc.; NSW uses CONTAINS while others use EQUALS. Solution: Resolved by B-4 consolidation. Source the state from a stable data-state="QLD" attribute on the link (ask the developer to add it) and trigger on Click Element — not on visible text.

B-12 — Inconsistent thank-you URL matching. Mix of Page URL vs Page Path; #20 matches the odd slug thank-you-participant; #111 matches the malformed slug https-whatability-com-au-thank-you-work-with-us-vic. Solution: Standardise every trigger to Page Path with an anchored RegEx. In WordPress, rename the malformed VIC thank-you page to a clean slug /thank-you-work-with-us-vic/ and align #20 to the standard pattern.

B-13 — Inconsistent event-name casing. need_support_SA vs need_support_nsw; work_with_us_SA vs work_with_us_wa — GA4 treats these as different events. Solution: Lowercase all state values. Moot once B-4 lands (state becomes a parameter) — just ensure the state parameter values are consistently lowercase.

B-14 — #185/#187 fire on DOM Ready. Onboardify iframe/forms may load asynchronously after DOM Ready, so the tag can run before the element exists. Solution: Switch the trigger to Element Visibility on the Onboardify iframe/link selector (fires when it actually appears), or to Window Loaded. Combine with the B-9 fix so #187 reads the cookie.

B.4 Low / housekeeping

FindingSolution
Folder named "a"Rename; organise all tags into meaningful folders (e.g. Config, Conversions, GCLID, Engagement).
No conversion values on GA4 event tagsAdd value + currency parameters per the Section 7.1 service value table. Required before tROAS bidding.
send_page_view: true on GA4 ConfigAcceptable — leave as is.

B.5 What is missing entirely

GapSolution
No Meta Pixel in the containerAdd the Meta Pixel base tag + conversion event tags per Sections 6.3 and 10.
No data-layer page contextNo page_type / service_name / location_name variables exist. Implement Layer 1 per Section 5 — Method A GTM JavaScript variables (§5.6, recommended) or Method B (§5.7).

B.6 Recommended remediation sequence

  1. Clean-out (do first, low risk): delete dead/duplicate items — B-2 (UA tags), B-6 (duplicate Conversion Linker), B-8 (dead CF7 path), B-7 (dead ajax_form_submit if Onboardify cannot emit it).
  2. Consent (compliance gate): B-1 + B-3 — implement Consent Mode v2 and gate all GA4/Ads tags. Do this before any new tags go live.
  3. Consolidate: B-4 — collapse per-state sprawl into parameterised tags. This also closes B-5, B-11, B-13.
  4. GCLID cleanup: B-9, B-10, B-14 — single cookie mechanism, conditional firing, robust triggers.
  5. Standardise: B-12 (thank-you URLs), B.4 housekeeping (folders, conversion values).
  6. Build new: B.5 — Meta Pixel, data-layer page context, then proceed with Section 17 Phases 1+.
Test every change in GTM Preview against the Section 16 checklist. Work in a new workspace; do not publish until validated.

Appendix C — WordPress Environment Audit

Folds in the whatability-datalayer-audit (21 May 2026): WordPress plugin inventory and how GTM / the data layer are actually injected. Determines the Method B install route (§5.7).

C.1 Plugin inventory

The site runs 32 plugins (24 active, 8 inactive). Those relevant to tracking and code injection:

Active

PluginVersionRelevance
Head & Footer Code1.5.6Injects the GTM container (HEAD + BODY snippets) — and the chosen home for the data layer (§5.7 Route B1)
SEOPress9.6Has its own HEAD/BODY/FOOTER custom-tracking fields — not used for GTM; avoid splitting tracking across plugins
Microsoft Clarity0.10.23Active heatmap / session recording — must be in consent scope (Risk R5)
Meta Tag Manager3.3Meta tag management
Advanced Custom Fields6.8.0Field management — usable for per-page data layer values if needed (R4)
Elementor / Elementor Pro3.35.8 / 3.35.1Page builder

Inactive

PluginVersionNote
WPCode Lite2.3.5Installed but NOT activated — no snippets configured. Method B Route B2 requires activating it first.
Query Monitor3.20.4Developer tool — inactive
Wordfence Security8.1.4Inactive
WP Accessibility Helper0.6.6Inactive

C.2 GTM installation — confirmed

GTM GTM-MCXFB7T is fully and correctly installed via the Head & Footer Code plugin:

  • HEAD snippet — present in the plugin's HEAD Code field (standard GTM loader + dataLayer reference).
  • BODY snippet — present in the BODY Code field (<noscript><iframe> fallback).
  • The BODY field also holds an unrelated custom <script> (a DOMContentLoaded breadcrumb-truncation script) — existing custom JS, harmless, noted for awareness.

This corrects the earlier assumption that GTM was installed via MonsterInsights. MonsterInsights configures GA4; the GTM container lives in Head & Footer Code.

C.3 Data layer install path — decision

Relevant only if Method B (§5.7) is chosen. Method A — the recommended approach (§5.5) — lives entirely in GTM and needs no WordPress install path.

OptionVerdict
Head & Footer Code pluginRecommended (§5.7 Route B1). Already active, already manages both GTM snippets — keeps all tracking code in one place. Paste the data layer script above the GTM snippet in the HEAD field.
WPCode LitePossible (§5.7 Route B2) but requires activating an otherwise-unused plugin — splits tracking management across two tools. Use only if a server-side PHP push is specifically wanted.
SEOPress custom trackingNot recommended — would fragment code across a third plugin.

C.4 Findings carried into the document

FindingWhere addressed
GTM injected via Head & Footer Code (not MonsterInsights)§3.1, §6.1, Appendix A.1 corrected
WPCode Lite inactive§5.7 Route B2 requires activation; §13.1 Scenario B note
Head & Footer Code is the recommended data layer home§5.7 Route B1
Microsoft Clarity active (session recording)§3.1 stack; Risks R5, R6 — consent scope
Data layer not explicitly pushed today (GTM only inits an empty one)§5.0; resolved by §5.7

End of master document.