Wix: AI-Powered Website Builder with App Marketplace | OpsBlu Docs

Wix: AI-Powered Website Builder with App Marketplace

Wix features a drag-and-drop editor, hundreds of templates, AI design tools, an extensive app marketplace, and integrated eCommerce.

Analytics Architecture on Wix

Wix is a closed platform. Unlike WordPress, Drupal, or any self-hosted CMS, you have no access to the server filesystem, no ability to modify HTTP response headers, and no way to run server-side code outside of Wix's own Velo runtime. Every analytics implementation on Wix works within these constraints, and understanding them upfront saves hours of debugging.

How Script Injection Works

Wix provides three mechanisms for adding tracking code:

  1. Settings > Custom Code -- The primary method. You paste JavaScript or HTML snippets into one of three injection points: Head, Body Start, or Body End. Wix wraps your code and injects it into the rendered page. You do not control load order beyond choosing the injection point.

  2. Marketing Integrations -- Native first-party integrations for Google Analytics, Facebook Pixel, Google Ads, and a handful of others. These are toggle-and-paste-ID setups that Wix manages internally. They handle SPA navigation automatically but give you almost no control over what gets sent.

  3. Wix Velo (formerly Corvid) -- A JavaScript development environment built into the Wix Editor. Velo gives you client-side page code, backend web modules, and access to Wix APIs (wix-window, wix-stores, wix-crm, etc.). This is the only way to programmatically interact with page elements, product data, or form submissions for analytics purposes.

Wix's Built-In Analytics

Wix Dashboard includes a native analytics panel that tracks visits, unique visitors, page views, traffic sources, and basic geography. This data is not exportable in raw form and cannot be joined with external analytics. Treat it as a sanity check, not a primary data source.

Single-Page Application Behavior

Wix sites use client-side navigation. When a visitor clicks a link to another page on your Wix site, the browser does not perform a full page load. The URL changes, the DOM updates, but window.onload does not fire again. This means any tracking script that relies on the traditional page load lifecycle will only record the initial landing page. This single behavior is responsible for the majority of analytics data loss on Wix sites.


Installing Tracking Scripts

Via Settings > Custom Code

Navigate to your Wix Dashboard, then Settings > Custom Code > Add Custom Code. You will be prompted to paste your code and select a placement.

Placement options:

  • Head -- Loads before the page renders. Use this for analytics base libraries (gtag.js, GTM container snippet, Meta Pixel base code). Scripts here block rendering if not loaded asynchronously.
  • Body - start -- Loads after the opening <body> tag. Use this for GTM's <noscript> fallback iframe.
  • Body - end -- Loads after the page content. Use this for non-critical tracking, heatmap tools, or event listeners that need DOM elements to exist.

Page targeting: You can apply Custom Code to all pages or specific pages. For site-wide analytics, always select "All pages" and "Load code once" (not "Load code on each new page" unless you specifically need re-execution on SPA navigations).

GA4 installation via Custom Code:

<!-- Placement: Head | Pages: All pages | Load: Once -->
<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', {
    send_page_view: false
  });
</script>

Note: send_page_view: false is intentional here. Because Wix is an SPA, the automatic pageview on script load only captures the landing page. You need a separate mechanism (covered in the GTM section below) to fire pageviews on subsequent navigations. If you leave the default send_page_view: true and also set up SPA navigation tracking, you will double-count the landing page.

Via Wix Marketing Integrations

Navigate to Dashboard > Marketing & SEO > Marketing Integrations. Wix offers native connectors for:

  • Google Analytics (GA4) -- paste your Measurement ID
  • Facebook Pixel -- paste your Pixel ID
  • Google Ads -- paste your Conversion ID

These integrations handle SPA navigation automatically. Wix fires virtual pageviews on client-side route changes without any additional configuration. The tradeoff is that you cannot customize event parameters, cannot add custom dimensions, and cannot control the timing or conditions under which events fire. For basic pageview and standard event tracking, these integrations are adequate. For anything beyond that, you need Custom Code or Velo.

Do not use both the native Marketing Integration for GA4 and a manual gtag.js installation via Custom Code. This is the most common source of duplicate pageview data on Wix sites.

Via Wix Velo

Velo is Wix's integrated development environment. Enable it from the Wix Editor by toggling Dev Mode. Once enabled, you can write client-side JavaScript that runs in the context of individual pages, site-wide code, or backend modules.

Velo's wixWindow.trackEvent() method fires events through Wix's native analytics pipeline, which feeds into any connected Marketing Integrations:

// Page code (runs on a specific page in the Wix Editor)
import wixWindow from 'wix-window';

$w.onReady(function () {
    wixWindow.trackEvent('CustomAction', {
        eventCategory: 'Engagement',
        eventAction: 'VideoPlay',
        eventLabel: 'Homepage Hero Video'
    });
});

This method works if you are using Wix's native Marketing Integrations. If you installed GA4 or GTM via Custom Code instead, wixWindow.trackEvent() will not reach your analytics. In that case, you need to push events to the dataLayer or use postMessage to communicate between the Velo sandbox and your Custom Code scripts.


Google Tag Manager on Wix

Installation

Add the GTM container snippet via Custom Code. You need two separate entries:

Entry 1 -- GTM JavaScript snippet:

  • Placement: Head
  • Pages: All pages
  • Load: Once
<!-- Google Tag Manager -->
<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>

Entry 2 -- GTM noscript fallback:

  • Placement: Body - start
  • Pages: All pages
  • Load: Once
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>

The SPA Navigation Problem

After GTM loads on the initial page, Wix handles all subsequent navigation client-side. GTM's built-in "All Pages" trigger (which fires on gtm.js) only fires once. Every page after the landing page is invisible to GTM unless you explicitly push a virtual pageview event.

The standard solution is a MutationObserver that watches for URL changes:

// Placement: Body - end | Pages: All pages | Load: Once
(function() {
    var lastUrl = location.href;
    var lastTitle = document.title;

    new MutationObserver(function() {
        var currentUrl = location.href;
        if (currentUrl !== lastUrl) {
            lastUrl = currentUrl;
            // Title updates asynchronously on Wix; wait briefly
            setTimeout(function() {
                window.dataLayer = window.dataLayer || [];
                dataLayer.push({
                    'event': 'wix_virtual_pageview',
                    'page_path': location.pathname + location.search,
                    'page_title': document.title,
                    'page_location': location.href
                });
            }, 150);
        }
    }).observe(document.querySelector('body'), {
        subtree: true,
        childList: true
    });
})();

In GTM, create a trigger:

  • Trigger type: Custom Event
  • Event name: wix_virtual_pageview

Use this trigger for your GA4 Configuration tag or as the pageview trigger for any tag that needs to fire on every navigation.

The 150ms setTimeout is not arbitrary. Wix updates the URL before it updates document.title. Without the delay, your pageview events will carry the previous page's title. On slower connections, you may need to increase this to 300ms or implement a polling check.

dataLayer Initialization

Wix does not guarantee execution order between multiple Custom Code entries at the same placement. If your MutationObserver script loads before the GTM snippet, the dataLayer array will exist but GTM will not be listening yet. The window.dataLayer = window.dataLayer || []; guard handles this -- events pushed before GTM loads are queued and processed when GTM initializes.


Form Tracking

Wix Forms App

Wix's built-in Forms app does not expose a JavaScript event on submission. The form submission is handled internally by Wix, and there is no DOM event you can reliably listen for. Using Velo is the most reliable approach:

// Page code (Velo) -- Attach to the page containing the Wix Form
import wixWindow from 'wix-window';

$w.onReady(function () {
    $w('#form1').onWixFormSubmit((event) => {
        const fields = event.fields;

        window.dataLayer = window.dataLayer || [];
        window.dataLayer.push({
            'event': 'form_submission',
            'form_id': 'contact_form',
            'form_name': 'Contact Us',
            'field_count': fields.length
        });
    });
});

Note: onWixFormSubmit fires after the form is successfully submitted to Wix's backend, not when the user clicks the submit button. This means you are tracking confirmed submissions, not attempts. If you need to track attempts (including validation failures), attach a click handler to the submit button instead.

Custom Forms Built with Velo

If you built a form using Velo input elements rather than the Wix Forms app, you have full control:

// Page code (Velo)
$w.onReady(function () {
    $w('#submitButton').onClick(async () => {
        const name = $w('#nameInput').value;
        const email = $w('#emailInput').value;
        const message = $w('#messageInput').value;

        // Validate before tracking
        if (!name || !email) {
            return;
        }

        // Push to dataLayer for GTM
        window.dataLayer = window.dataLayer || [];
        window.dataLayer.push({
            'event': 'generate_lead',
            'form_name': 'Custom Contact Form',
            'method': 'wix_velo_form'
        });

        // Then handle the actual submission
        // (send to collection, external API, etc.)
    });
});

Communicating Between Velo and Custom Code

Velo code runs in a sandboxed iframe. If you push to window.dataLayer from Velo page code, you are pushing to the Velo iframe's dataLayer, not the parent page's dataLayer where GTM is listening. To bridge this gap, use postMessage:

// In Velo page code
import wixWindow from 'wix-window';

$w.onReady(function () {
    $w('#submitButton').onClick(() => {
        // Send message to parent window where GTM lives
        wixWindow.postMessage({
            type: 'analytics_event',
            event: 'generate_lead',
            form_name: 'Contact Form'
        }, '*');
    });
});

Then in a Custom Code entry (Body - end, all pages), listen for these messages:

<script>
window.addEventListener('message', function(event) {
    if (event.data && event.data.type === 'analytics_event') {
        window.dataLayer = window.dataLayer || [];
        window.dataLayer.push({
            'event': event.data.event,
            'form_name': event.data.form_name
        });
    }
});
</script>

This two-step pattern is necessary for any Velo-to-GTM communication. Direct dataLayer.push() from Velo code will silently fail to reach GTM.


Wix Stores (eCommerce) Tracking

Wix Stores provides built-in eCommerce analytics visible in the Wix Dashboard, but the data stays inside Wix. For GA4 Enhanced Ecommerce, you need to extract product data using Velo and push it to the dataLayer.

Product Page View

// Product page code (Velo)
import wixWindow from 'wix-window';

$w.onReady(function () {
    $w('#productPage1').getProduct()
        .then((product) => {
            wixWindow.postMessage({
                type: 'analytics_event',
                event: 'view_item',
                ecommerce: {
                    currency: product.currency || 'USD',
                    value: product.discountedPrice || product.price,
                    items: [{
                        item_id: product.sku || product._id,
                        item_name: product.name,
                        price: product.discountedPrice || product.price,
                        item_category: product.productType,
                        quantity: 1
                    }]
                }
            }, '*');
        })
        .catch((err) => {
            console.error('Failed to get product data for analytics:', err);
        });
});

Add to Cart

Wix Stores does not fire a DOM event when a product is added to the cart through the default "Add to Cart" button. The button is rendered inside a Wix widget that you cannot directly attach event listeners to from Velo in all cases. One workaround is to use the wix-stores cart API to detect cart changes:

// Site-level code (masterPage.js in Velo)
import wixStores from 'wix-stores';
import wixWindow from 'wix-window';

wixStores.onCartChanged((cart) => {
    if (cart.totals.quantity > 0) {
        const lastItem = cart.lineItems[cart.lineItems.length - 1];
        wixWindow.postMessage({
            type: 'analytics_event',
            event: 'add_to_cart',
            ecommerce: {
                currency: cart.currency || 'USD',
                value: lastItem.totalPrice,
                items: [{
                    item_id: lastItem.sku || lastItem.productId,
                    item_name: lastItem.name,
                    price: lastItem.price,
                    quantity: lastItem.quantity
                }]
            }
        }, '*');
    }
});

This fires on every cart change, not just additions. If the user updates quantity or removes an item, onCartChanged fires again. You need logic to differentiate additions from removals if you want accurate add_to_cart vs remove_from_cart events. Track the previous cart state and compare.

Checkout Limitations

Wix's checkout flow is entirely closed. Once a visitor clicks "Checkout," they enter a Wix-managed page sequence that you cannot inject Custom Code into and cannot access via Velo. This means:

  • You cannot fire begin_checkout, add_shipping_info, or add_payment_info events.
  • You cannot fire a purchase event at the moment of transaction.
  • The Wix "Thank You" page after checkout does allow Custom Code, but product-level data is not available in the DOM.

If you use Wix's native GA4 Marketing Integration, Wix handles firing a purchase event internally during checkout. This is one of the stronger arguments for using the native integration alongside a manual GTM setup, even though it adds configuration complexity.


Wix includes a GDPR-compliant cookie banner that can be enabled from Dashboard > Settings > Cookie Banner. When enabled, Wix categorizes cookies into Essential, Functional, Analytics, and Advertising, and blocks non-essential cookies until the visitor consents.

For Custom Code scripts, Wix will delay injection of scripts you tag as requiring consent. When adding Custom Code, you can assign it to a consent category. If the visitor has not consented to that category, the script will not load.

You can check and react to consent changes programmatically:

// Site-level code (masterPage.js in Velo)
import { onConsentPolicyChanged, getCurrentConsentPolicy } from 'wix-window-frontend';

// Check initial consent state
const policy = getCurrentConsentPolicy();
if (policy.analytics) {
    // Analytics consent granted, safe to fire tracking
}

// React to consent changes
onConsentPolicyChanged((event) => {
    if (event.policy.analytics) {
        window.dataLayer = window.dataLayer || [];
        window.dataLayer.push({
            'event': 'consent_update',
            'analytics_consent': 'granted'
        });
    }
});

If you are using GTM with Google Consent Mode v2, you need to map Wix's consent categories to GTM's consent signals (analytics_storage, ad_storage, etc.). Wix's cookie banner does not natively emit Google Consent Mode signals, so you must bridge them manually through the dataLayer.


Common Errors

Error / Symptom Cause Fix
Pageview only fires once, not on subsequent navigation Wix SPA navigation does not trigger gtm.js or window.onload after the initial page load Implement the MutationObserver pattern (see GTM section) or use Wix's native GA4 Marketing Integration which handles this automatically
Custom Code not appearing on published site Code was added in the Editor but the site was not republished Click "Publish" in the Wix Editor after adding or modifying any Custom Code entry
gtag('config') fires but no data appears in GA4 Measurement ID is incorrect, or the GA4 property is newly created and not yet processing Verify the Measurement ID starts with G- and matches the correct GA4 property; allow 24-48 hours for new properties to show data
Velo code not executing Dev Mode is not enabled, or there is a syntax error in the page code Enable Dev Mode in the Wix Editor toolbar; open the Velo console (bottom panel) and check for runtime errors
postMessage from Velo not received by GTM The Custom Code listener for message events is missing or has a filtering condition that rejects the message Add a message event listener in a Custom Code entry (see the Velo-to-GTM bridging pattern above); verify the event.data.type matches
Duplicate pageviews in GA4 Both the Wix native GA4 Marketing Integration and a manual gtag.js snippet are active simultaneously Remove one: either use the Marketing Integration exclusively, or disconnect it and rely on Custom Code with manual SPA tracking
Cross-domain tracking broken between Wix site and external checkout or subdomain The _gl linker parameter is stripped during redirects, or the external domain is not configured in GA4's cross-domain settings Add all domains to GA4 Admin > Data Streams > Configure Tag Settings > Configure Your Domains; verify the redirect preserves query string parameters
Custom Code loads too late, missing early interactions Tracking script placed in Body - end instead of Head Move the base analytics library (gtag.js, GTM snippet) to the Head placement; keep event listeners in Body - end
wix-stores API returns undefined product data The product page widget has not finished loading when Velo code runs Use $w('#productPage1').getProduct() inside $w.onReady(), or chain with .then() to ensure the product data promise resolves before accessing properties
Events fire in GTM Preview mode but not in production GTM container has not been published, or the workspace version differs from the live version In GTM, click "Submit" to publish the workspace; verify the container version number matches between Preview and Published

Wix Limitations for Analytics

These are hard platform constraints that no amount of configuration can work around:

No server-side tracking. Wix does not expose server-side request handling. You cannot implement Measurement Protocol calls from the server, cannot run a server-side GTM container, and cannot perform server-side cookie setting. All tracking is client-side only, which means ad blockers and browser privacy features will block a portion of your data.

No HTTP header access. You cannot set or read custom HTTP response headers. Content Security Policy headers, CORS headers, and cache-control directives are managed entirely by Wix. This limits your ability to implement certain security-oriented tracking configurations.

Closed checkout flow. The Wix Stores checkout sequence does not allow Custom Code injection. You lose visibility into checkout steps unless you use Wix's native Marketing Integration, which fires a limited set of ecommerce events internally.

No raw data export from Wix Analytics. The built-in Wix Analytics dashboard does not offer CSV export, API access, or BigQuery integration. If you need raw session or event data, you must collect it through an external analytics platform.

No direct DOM access to Wix widgets. Many Wix components (galleries, forms, stores widgets) render inside iframes or shadow DOM structures that Custom Code cannot reach. Velo provides API methods for some of these widgets, but not all.

Script execution order is not guaranteed. Multiple Custom Code entries at the same placement (e.g., two scripts both in Head) do not have a guaranteed execution order. If script B depends on script A, you cannot reliably ensure A loads first. Combine them into a single Custom Code entry or use event-driven initialization.


Performance Considerations

Wix controls hosting, CDN, and page rendering. Your influence over site performance is limited to what you add, not what Wix provides.

Every Custom Code entry adds a network request and execution time. On Wix specifically, there is additional overhead because Wix wraps your code in its own injection framework. Five or six Custom Code entries (GTM, GA4, Meta Pixel, heatmap tool, chat widget, consent management) can add 200-500ms to the initial page load depending on the scripts involved.

The "Load code on each new page" option in Custom Code causes the script to re-execute on every SPA navigation. This is rarely what you want for analytics scripts, which should load once and then listen for navigation events. Use "Load code once" for analytics base libraries and reserve "Load code on each new page" only for scripts that genuinely need to reinitialize per page.

Wix Editor X (now Wix Studio) uses a different rendering engine than the classic Wix Editor. Custom Code behavior, timing, and DOM structure may differ between the two. Test your tracking implementation on the actual published site, not just in the Editor preview, which runs in a different context.

Third-party script loading on Wix is subject to Wix's own resource prioritization. Your scripts may be deferred or deprioritized if Wix determines they are competing with core platform resources. There is no way to override this behavior.