Analytics Architecture on Squarespace
Squarespace is a closed platform. You cannot modify server configuration, edit HTTP headers, or access the file system. Every piece of tracking code enters through a few narrow injection points, and understanding those boundaries is the first requirement for reliable analytics.
Code Injection Points
Settings > Advanced > Code Injection provides four locations:
- Site-wide Header: Renders inside
<head>on every page. Place GTM, GA4, and consent scripts here. - Site-wide Footer: Renders before
</body>on every page. Use for noscript fallbacks and DOM-dependent code. - Per-page Header: Available in individual page settings. Injects into
<head>for that page only. - Per-page Footer: Same per-page scope, renders before
</body>.
Commerce plans have a fifth injection point: Commerce > Advanced > Order Confirmation Page, which executes on the post-purchase screen and has access to order-specific template variables.
Squarespace Built-in Analytics
The native analytics panel tracks: unique visitors, sessions, traffic sources, geography, popular content, and search keywords. Commerce plans add revenue, orders, and conversion rate.
What it does not track: custom events, scroll depth, outbound clicks, video engagement, form abandonment, or ecommerce funnel steps (add to cart, begin checkout). The built-in analytics is server-side log analysis, not client-side behavioral tracking.
Squarespace 7.0 vs 7.1 Navigation Model
This distinction matters more for analytics than for design.
Squarespace 7.0 uses traditional full-page navigation. Standard analytics triggers (GTM's All Pages, GA4's page_view) work as expected.
Squarespace 7.1 uses AJAX navigation powered by Mercury. When a visitor clicks a link, Squarespace fetches new content via XHR and swaps the DOM without a full page load. The URL changes, the content changes, but window.onload does not fire again. This means:
- GTM's built-in "All Pages" trigger fires only once (the initial page load)
- GA4's automatic
page_viewevent fires only once - Any script relying on
DOMContentLoadedorwindow.onloadruns only on first arrival
This is the single most common source of analytics data loss on Squarespace. If GA4 reports show unusually low pages-per-session, this is almost certainly the cause.
Installing Tracking Scripts
Via Code Injection (Recommended)
Navigate to Settings > Advanced > Code Injection and paste your tracking snippet into the Header field:
<!-- Code Injection > Header -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>
This works for the initial page load. On 7.1 sites, you need additional AJAX handling (covered in the GTM section below).
Via Built-in Integrations
Squarespace offers native connectors under Settings > Marketing > Analytics:
- Google Analytics: Enter your Measurement ID. Squarespace injects gtag.js automatically.
- Facebook Pixel: Enter your Pixel ID.
- Pinterest Tag: Enter your Tag ID.
These built-in integrations have significant limitations:
- They fire basic pageviews only. No custom events, no ecommerce events, no enhanced measurement.
- You cannot pass custom parameters or dimensions.
- You cannot control when they fire (no consent gating without additional work).
- The Facebook Pixel integration does not support Conversions API (server-side).
- GA4's enhanced measurement settings (scroll tracking, outbound clicks, site search) cannot be configured through the native connector.
For anything beyond basic pageview tracking, use Code Injection with GTM instead.
Via Developer Mode (7.0 Only)
Squarespace 7.0 offered Developer Mode with access to template files (JSON-T format), allowing direct control over script placement. Developer Mode does not exist in 7.1. Migrating to 7.1 removes this access permanently.
Google Tag Manager on Squarespace
Installation
Add the GTM container snippet to Code Injection. JavaScript goes in Header, noscript fallback in Footer:
<!-- Code Injection > Header -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
<!-- Code Injection > Footer -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
Replace GTM-XXXXXXX with your container ID. The noscript iframe goes in the Footer because Header injection renders inside <head>, and noscript iframes are body-level elements.
Handling AJAX Navigation in 7.1
On 7.1, listen for Mercury navigation events and push a custom event to the dataLayer. Place this after the GTM snippet in Code Injection > Header:
<script>
window.addEventListener('mercury:load', function() {
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'event': 'squarespace_page_view',
'page_path': window.location.pathname,
'page_title': document.title
});
});
</script>
The mercury:load event fires every time Squarespace completes an AJAX page transition. It does not fire on the initial page load.
In GTM, create a Custom Event trigger with event name squarespace_page_view. Use this trigger for all tags that should fire on every page (GA4, Facebook PageView, etc.). Keep the standard "All Pages" trigger as well so tags fire on both initial load and subsequent navigations.
For GA4 specifically, send a manual page_view on each Mercury navigation:
<script>
window.addEventListener('mercury:load', function() {
if (typeof gtag === 'function') {
gtag('event', 'page_view', {
page_title: document.title,
page_location: window.location.href,
page_path: window.location.pathname
});
}
});
</script>
If using GTM rather than direct gtag, handle this through a GA4 Event tag triggered by squarespace_page_view instead of inline gtag() calls.
Squarespace Commerce Tracking
Built-in Commerce Analytics
Commerce plans include a dashboard tracking revenue, orders, units sold, average order value, and conversion rate. This data lives exclusively inside Squarespace and is not forwarded to GA4, GTM, or any external tool.
GA4 Enhanced Ecommerce
Squarespace does not push ecommerce events into the dataLayer. There is no native view_item, add_to_cart, or begin_checkout event. You need to extract product data from the DOM:
<!-- Code Injection > Footer (fires on product pages) -->
<script>
document.addEventListener('DOMContentLoaded', function() {
var productTitle = document.querySelector('.ProductItem-details h1, .pdp-details h1');
var priceEl = document.querySelector('.product-price .sqs-money-native, .pdp-price .sqs-money-native');
if (productTitle && priceEl) {
var price = parseFloat(
priceEl.getAttribute('data-value') ||
priceEl.textContent.replace(/[^0-9.]/g, '')
);
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'event': 'view_item',
'ecommerce': {
'currency': 'USD',
'value': price,
'items': [{
'item_name': productTitle.textContent.trim(),
'price': price,
'quantity': 1
}]
}
});
}
});
</script>
Verify the selectors against your specific template, as Squarespace periodically changes class names.
Order Confirmation Tracking
Commerce > Advanced > Order Confirmation Page has its own code injection with access to order template variables:
<!-- Commerce > Advanced > Order Confirmation Page -->
<script>
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'event': 'purchase',
'ecommerce': {
'transaction_id': '{orderId}',
'value': parseFloat('{orderSubtotal}'.replace(/[^0-9.]/g, '')),
'currency': 'USD',
'tax': parseFloat('{orderTaxTotal}'.replace(/[^0-9.]/g, '')),
'shipping': parseFloat('{orderShippingTotal}'.replace(/[^0-9.]/g, '')),
'items': []
}
});
</script>
Available template variables: {orderId} (order number), {orderSubtotal} (subtotal, rendered with currency symbol), {orderTaxTotal}, {orderShippingTotal}, {orderGrandTotal}, and {customerEmailAddress} (handle with care under privacy regulations).
These variables provide order-level totals only. There is no template variable for individual line items (product names, quantities, per-item prices). The items array will be empty unless you use the Squarespace Commerce API client-side, which requires OAuth and is impractical from a confirmation page script.
For businesses that need line-item purchase data in GA4, the workaround is to track add_to_cart events on the product page and use GA4's data model to associate items with transactions, accepting that the purchase event itself will lack individual item detail.
Form Tracking
Squarespace Form Blocks do not emit JavaScript events on submission. The form submits via AJAX and shows a confirmation message by replacing the form DOM.
To track form submissions, listen for the submit event at the document level:
<!-- Code Injection > Footer -->
<script>
document.addEventListener('submit', function(e) {
var form = e.target;
if (form.closest('.sqs-block-form')) {
var formBlock = form.closest('.sqs-block-form');
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'event': 'form_submit',
'form_id': formBlock.getAttribute('data-block-json') ?
JSON.parse(formBlock.getAttribute('data-block-json')).id : 'unknown',
'page_path': window.location.pathname
});
}
}, true);
</script>
The true parameter enables capture phase listening, necessary because Squarespace's handlers may call stopPropagation().
An alternative approach uses a MutationObserver to detect when the form is replaced by the success message:
<!-- Code Injection > Footer -->
<script>
(function() {
var forms = document.querySelectorAll('.sqs-block-form');
forms.forEach(function(block) {
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
var successMsg = block.querySelector('.form-submission-text, .form-submission-html');
if (successMsg && successMsg.offsetParent !== null) {
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'event': 'form_submission_success',
'page_path': window.location.pathname
});
observer.disconnect();
}
});
});
observer.observe(block, { childList: true, subtree: true });
});
})();
</script>
The MutationObserver approach is more reliable because it fires after confirmed successful submission (success message appears), not on the submit action which may fail validation.
Consent Management
Squarespace includes a built-in Cookie Banner (Settings > Cookies & Visitor Data) that provides basic accept/decline functionality but limited tag management integration. The banner sets a cookie named ss_cookieAllowed with a value of true or false:
<!-- Code Injection > Header (before GTM) -->
<script>
function getSquarespaceCookieConsent() {
var match = document.cookie.match(/ss_cookieAllowed=([^;]+)/);
return match ? match[1] === 'true' : null;
}
var consent = getSquarespaceCookieConsent();
if (consent === true) {
// Load GTM or other tracking scripts
} else if (consent === null) {
// No choice made yet; wait for banner interaction
// Squarespace reloads the page after consent choice
}
</script>
For granular consent (separating analytics from marketing cookies), Squarespace's built-in banner is insufficient. Use a third-party CMP (OneTrust, Cookiebot, CookieYes) injected via Code Injection, with GTM configured to use consent mode signals.
Common Errors
| Error / Symptom | Cause | Fix |
|---|---|---|
| Pageviews only fire on the first page load, subsequent pages show no data | Squarespace 7.1 uses AJAX navigation (Mercury). Standard pageview triggers only fire once. | Add a mercury:load event listener that pushes a custom event to the dataLayer. Use that custom event as your trigger in GTM for all pageview-dependent tags. |
| Code Injection content not saving or disappearing | Script contains a syntax error, unclosed tag, or an element Squarespace strips (such as bare <iframe> or <object> tags). |
Validate your HTML/JS before pasting. Wrap iframes in the noscript context if needed. Check the browser console after saving for injection errors. |
| GA4 showing 40-60% fewer sessions than Squarespace Analytics | Squarespace's built-in analytics counts server-side hits including bots, crawlers, and visitors with JavaScript disabled. GA4 requires JavaScript execution. | This discrepancy is expected. Squarespace's numbers are inflated relative to any client-side analytics tool. Do not treat Squarespace's number as ground truth. |
| Purchase event fires but revenue is $0 or NaN | Squarespace template variables like {orderSubtotal} render with a currency symbol (e.g., $45.00). Passing this directly as a number fails. |
Always parse: parseFloat('{orderSubtotal}'.replace(/[^0-9.]/g, '')). |
| GTM Preview/Debug mode not working on Squarespace | The GTM debug signal does not persist across Mercury AJAX navigations, so Preview mode drops after the first page. | Append ?gtm_debug=x to the URL. If that fails, temporarily disable AJAX loading by adding <script>window.Squarespace.AFTER_BODY_LOADED = false;</script> to Code Injection during debugging (remove after). |
| Duplicate pageviews in GA4 | Both the native Squarespace GA integration (under Marketing > Analytics) and a manually installed GA4 tag (via Code Injection or GTM) are active simultaneously. | Use one or the other. If you use GTM, remove the Measurement ID from Squarespace's built-in analytics settings. |
mercury:load event not firing |
The site is running Squarespace 7.0, which uses full page loads instead of AJAX navigation. | No fix needed. On 7.0, standard GTM page view triggers work correctly. The mercury event listener is only necessary on 7.1. |
| Custom CSS not applying to the cookie consent banner | Squarespace renders the Cookie Banner inside a Shadow DOM, making it immune to site-wide CSS. | Target the banner using ::part() CSS selectors if the shadow DOM exposes parts, or inject a <style> element inside the shadow root via JavaScript. |
| Order Confirmation code not executing | Code is placed in site-wide Code Injection instead of Commerce > Advanced > Order Confirmation Page, or there is a JavaScript error in the snippet. | Verify the code is in the correct location. Test by placing a simple console.log('confirmation fired') first to confirm execution. |
| Facebook Pixel reporting zero events after initial setup | The built-in Facebook Pixel integration fires only on full page loads. On 7.1, AJAX navigations produce no Pixel events. | Remove the built-in integration. Install the Pixel via GTM with a squarespace_page_view custom event trigger, or add a manual fbq('track', 'PageView') call inside the mercury:load listener. |
Squarespace Limitations for Analytics
Hard platform constraints that cannot be worked around:
- No server-side tracking. No GTM Server-Side container, no Measurement Protocol, no Conversions API. Client-side only.
- No template file editing on 7.1. Developer Mode was a 7.0 feature and is permanently unavailable on 7.1.
- No line-item ecommerce data on order confirmation. Template variables provide order totals but not individual product names, SKUs, or quantities.
- No HTTP header modification. You cannot set Content-Security-Policy, Strict-Transport-Security, or Permissions-Policy. Squarespace controls all headers.
- No custom checkout flow. Checkout is a Squarespace-controlled iframe. You cannot inject scripts into checkout, so add-to-cart and begin-checkout tracking requires DOM scraping.
- Cookie banner does not support Google Consent Mode v2 categories (ad_storage, analytics_storage) without a third-party CMP.
- No cron jobs or scheduled scripts. Data collection must be triggered by user actions or page loads.
- URL structure is partially fixed. Squarespace enforces its own URL patterns for commerce (
/store/p/product-name) and blog (/blog/post-title). You can customize slugs but not the path structure.
Squarespace 7.0 vs 7.1 Tracking Reference
| Feature | 7.0 | 7.1 |
|---|---|---|
| Page navigation | Full page loads | AJAX via Mercury |
| Developer Mode | Available (JSON-T) | Not available |
| GTM "All Pages" trigger | Works on every navigation | Fires only on initial load |
| Custom event listener needed | No | Yes (mercury:load) |
| Code Injection | Header, Footer, Per-page | Header, Footer, Per-page |
| Template file access | Yes | No |
| AJAX-aware scripts needed | No | Yes |
| DOM class name stability | Stable per template | Changes more frequently |
| Migration path | Can upgrade to 7.1 | Cannot downgrade to 7.0 |
Determining whether a site runs 7.0 or 7.1 is the first step. Check Settings > Site > Site Version, or look for <meta property="squarespace:version" content="7.1"> in the page source.
Debugging Tips
Check whether Mercury is active by running this in the browser console:
// Returns true if AJAX navigation is active
typeof window.Squarespace !== 'undefined' &&
typeof window.Squarespace.Mercury !== 'undefined'
Monitor all dataLayer pushes in real time:
// Paste in browser console
(function() {
var origPush = Array.prototype.push;
window.dataLayer = window.dataLayer || [];
window.dataLayer.push = function() {
console.log('[dataLayer]', arguments[0]);
return origPush.apply(this, arguments);
};
})();
Verify which Code Injection scripts are rendering by searching the page source (Ctrl+U) for your expected content. Squarespace strips certain elements silently.
For intermittent tracking issues on 7.1, test with AJAX navigation disabled temporarily:
<script>
if (window.history && window.history.pushState) {
window.history.pushState = function() {
window.location.href = arguments[2];
};
}
</script>
This forces full page loads on every navigation, letting you isolate AJAX-related issues. Remove after debugging.