Full Stack Implementation
This page documents the end-to-end setup for a client landing page: Cloudflare DNS, Stape, GTM (web + server), BigQuery, Google Ads events, Webflow custom code, and testing. Use it alongside the Server-Side Tracking Guide for detailed Stape/GTM/BigQuery steps.
Architecture overview
Flow: User hits the landing page (e.g. get.clientdomain.com) hosted on Webflow. The page loads the GTM custom loader from Stape (first-party subdomain). The loader runs the web container; events are sent to the server container (sGTM) on Stape, which routes to GA4, Google Ads, and BigQuery.
Phase summary
| Phase | What | Outcome |
|---|---|---|
| 1 | Cloudflare DNS | Subdomain for Webflow + Stape CNAMEs |
| 2 | Stape | Server container + custom loader domains |
| 3 | GTM | Web + server containers; user-id variable |
| 4 | BigQuery | Dataset + table; Stape integration |
| 5 | Google Ads events | page_view, cta_click, lead_form_submit |
| 6 | Webflow code | GTM loader, user-id script, form hidden field, CTA tracking |
| 7 | Cal.com | Verify user-id in embed; optional booking event |
| 8 | Testing | GTM Preview, sGTM, GA4, Ads, BigQuery |
| 10 (later) | Offline conversions | n8n + Google Ads API |
Phase 1: Cloudflare DNS
Once the domain is in Cloudflare (see Migrating from another CMS if the client site is on Wix), add these records. Use your actual subdomain (e.g. get for get.clientdomain.com).
DNS records
| Type | Name | Target | Proxy | Purpose |
|---|---|---|---|---|
| CNAME | get | proxy-ssl.webflow.com | Proxied (orange) | Landing page (Webflow) |
| CNAME | data.get | eue.stape.net | DNS only | Stape sGTM server |
| CNAME | load.data.get | leue.stape.net | DNS only | Stape custom loader |
Alternative (root-level Stape): Use data and load.data as names if you prefer data.clientdomain.com and load.data.clientdomain.com.
SSL
- SSL/TLS mode: Full (not Flexible).
- Enable Always Use HTTPS for the zone.
- HSTS optional.
Phase 2: Stape
| Step | Action |
|---|---|
| 1 | Create new container in Stape (EU region). Note container identifier. |
| 2 | Set server location (e.g. EU North). |
| 3 | Add custom domain: data.get.clientdomain.com (or data.clientdomain.com). |
| 4 | Add custom loader domain: load.data.get.clientdomain.com (or load.data.clientdomain.com). |
| 5 | Enable power-ups: Custom Loader, Cookie Keeper, Click ID Restorer, Google Service Account (BigQuery), Bot Detection, GEO Headers, User Agent Info. |
SSL for the custom domains is provisioned by Stape.
Phase 3: GTM containers
Web container
- Create Web container. Note ID (
GTM-XXXXXXX). - Add Server Google Tag (config): Measurement ID (GA4), Tracking Server URL
https://data.get.clientdomain.com(or your Stape domain). Trigger: Consent Initialization (or All Pages if no CMP). - Add user-id variable: e.g.
_c_xxx(client-specific suffix). Priority: URL param → localStorage → cookie → generate. Store in both localStorage and cookie. - Add Client ID set tag: writes user-id on page load. Trigger: same as config tag.
Server container
- Create Server container. Note ID.
- Link to Stape using the container identifier; set Tracking Server URL in Stape to your Stape domain.
- In sGTM: route all events to GA4; route
lead_form_submit(and any other conversion events) to Google Ads; send all events to BigQuery tag.
Phase 4: BigQuery
| Item | Value |
|---|---|
| Dataset | e.g. client_events (location: EU) |
| Table | events (or equivalent) |
| Partition | DATE(event_timestamp) |
| Cluster | event_name, user_id |
Suggested schema (core columns)
| Column | Type | Notes |
|---|---|---|
event_name | STRING | page_view, cta_click, lead_form_submit, etc. |
event_timestamp | TIMESTAMP | |
user_id | STRING | From client-id variable (e.g. _c_xxx) |
session_id | STRING | |
page_url | STRING | |
page_title | STRING | |
utm_source, utm_medium, utm_campaign, utm_term, utm_content | STRING | |
device_type, browser, os | STRING | |
country, city | STRING | |
event_data | JSON | Event-specific payload |
created_at | TIMESTAMP |
Enable the Google Service Account power-up in Stape and connect it to this BigQuery dataset.
Phase 5: Google Ads events
GA4
- Create (or use existing) GA4 property. Set timezone and currency. Link Google Ads to the property.
Events to implement
| Event | Trigger | Typical use |
|---|---|---|
page_view | All Pages (after consent) | Default; can be used as secondary conversion |
cta_click | Click on CTA elements (class/ID or data attribute) | Primary or secondary conversion |
lead_form_submit | Form submit on lead form | Primary conversion for Google Ads |
GTM web container
- Page View: GA4 Event via sGTM; event name
page_view; parameters e.g.page_location,page_title. Trigger: All Pages. - CTA Click: GA4 Event via sGTM; event name
cta_click; parameters e.g.cta_text,cta_location,page_url,user_id. Trigger: Click on elements withdata-cta-tracking="true"(or chosen class/ID). - Form Submit: GA4 Event via sGTM; event name
lead_form_submit; parameters e.g.form_name,form_id,user_id. Trigger: Form submission on the lead form.
sGTM
- Route all three events to GA4 and to the BigQuery logging tag.
- Map
lead_form_submitto a Google Ads conversion (Conversion ID + label from the Ads account). Enable Conversion Linker in both web and server containers.
Phase 6: Webflow code snippets
6.1 GTM loader (Head)
In Webflow: Site settings → Custom Code → Head Code.
<script src="https://load.data.get.clientdomain.com/gtm.js?id=GTM-XXXXXXX"></script>
Replace load.data.get.clientdomain.com with your Stape loader domain and GTM-XXXXXXX with your web container ID.
6.2 Noscript (Body)
Site settings → Custom Code → Body Code (top or bottom):
<noscript><iframe src="https://load.data.get.clientdomain.com/ns.html?id=GTM-XXXXXXX"></iframe></noscript>
6.3 User-id script (Footer or Head)
Use a client-specific cookie/localStorage name (e.g. _c_xxx). Priority: URL param → localStorage → cookie → generate.
<script>
(function() {
var COOKIE_NAME = '_c_xxx';
var urlParam = new URLSearchParams(window.location.search).get('c_xxx');
var lsVal = localStorage.getItem(COOKIE_NAME);
var cookieMatch = document.cookie.match(new RegExp('(?:^|; )' + COOKIE_NAME + '=([^;]*)'));
var cookieVal = cookieMatch ? cookieMatch[1] : null;
var cid = urlParam || lsVal || cookieVal || (function() {
var id = 'cid_' + Date.now() + '_' + Math.random().toString(36).slice(2, 11);
try { localStorage.setItem(COOKIE_NAME, id); } catch (e) {}
document.cookie = COOKIE_NAME + '=' + id + ';path=/;max-age=31536000;SameSite=Lax';
return id;
})();
if (!lsVal && cid) try { localStorage.setItem(COOKIE_NAME, cid); } catch (e) {}
if (!cookieMatch && cid) document.cookie = COOKIE_NAME + '=' + cid + ';path=/;max-age=31536000;SameSite=Lax';
})();
</script>
Replace _c_xxx and c_xxx with your client’s ID name.
6.4 Form hidden field
- Add a Hidden field to the Webflow form. Name/ID: e.g.
hidden-field1. - Populate it on load (after user-id script). Example, using same
COOKIE_NAMEas above:
<script>
window.addEventListener('load', function() {
setTimeout(function() {
var COOKIE_NAME = '_c_xxx';
var lsVal = localStorage.getItem(COOKIE_NAME);
var cookieMatch = document.cookie.match(new RegExp('(?:^|; )' + COOKIE_NAME + '=([^;]*)'));
var cid = lsVal || (cookieMatch ? cookieMatch[1] : null) || 'nocookie';
var field = document.querySelector('#hidden-field1') || document.querySelector('input[name="hidden-field1"]');
if (field) field.value = cid;
}, 1000);
});
</script>
6.5 CTA click tracking
- On each CTA link/button, add attributes:
data-cta-tracking="true",data-cta-text="Button label",data-cta-location="hero"(or similar). - Either use GTM’s built-in Click trigger (Click – All Elements, filter by
data-cta-tracking= true) and pass click element text/attributes into the tag, or push adataLayerevent on click and trigger the GA4 event from that.
Example dataLayer push (if you prefer to fire from custom JS):
document.querySelectorAll('[data-cta-tracking="true"]').forEach(function(el) {
el.addEventListener('click', function() {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event: 'cta_click',
cta_text: el.getAttribute('data-cta-text') || el.textContent.trim(),
cta_location: el.getAttribute('data-cta-location') || '',
page_url: window.location.href
});
});
});
Phase 7: Cal.com (and similar embeds)
- User-id: Pass the same client-id (e.g.
_c_xxx) into the Cal.com embed via its prefill/hidden field (see Third-party embeds and tracking). - Booking conversion: Optionally fire a
cal_booking_complete(or similar) event via Cal.com webhook to sGTM or via PostMessage to the parent page and thendataLayer.push; includeuser_idfor attribution.
Phase 8: Testing
| Check | Where |
|---|---|
| GTM Preview: page_view, cta_click, lead_form_submit fire | GTM Preview mode |
| User-id present in events and in form hidden field | GTM variables / form submit payload |
| Events reach sGTM | Stape dashboard / sGTM debug |
| GA4 Real-Time shows events | GA4 → Reports → Real-time |
| Google Ads conversion (lead_form_submit) | Ads account; may lag 24–48 h |
| Rows in BigQuery | Query events table |
| Cross-browser (Chrome, Safari, Firefox) | Manual test |
| Form submit includes user-id | Backend or form tool logs |
See Server-Side Tracking – Testing & Validation for more detail.
Phase 10 (later): Offline conversions
- Design workflow (e.g. CRM or sheet → n8n → Google Ads Upload API).
- Use the same user-id / click-id where possible for matching.
- See Google Ads (tool) for existing offline conversion patterns.
Related docs
| Topic | Doc |
|---|---|
| GTM, user-id, form pattern | GTM (tool) |
| Stape setup | Stape (tool) |
| DNS, SSL | Cloudflare (tool) |
| BigQuery | BigQuery (tool), BigQuery integration |
| Google Ads conversions | Google Ads (tool) |
| End-to-end sGTM | Server-Side Tracking Implementation |