Site builder

Server-Side Tracking for Framer

Stop losing conversions to iOS ITP, short-lived cookies, and ad-blockers. TrakSolv hosts a first-party server-side GTM container for your Framer store and forwards clean, deduplicated conversions to GA4, Meta CAPI, Google Ads and more. Live in minutes, flat monthly pricing.

No credit card to startFlat monthly pricingNative Framer dataLayerGA4 · Meta CAPI · Google Ads
Why it matters for Framer

The pixel alone is leaking your data.

Browser-side tracking on Framer loses a growing share of conversions. Here's what moving to a server-side setup fixes.

Recover conversions lost to ITP & ad-blockers

On Framer, browser-side pixels are throttled by Safari/Firefox ITP, capped 7-day cookies, and blocked outright by ad-blockers and consent walls. Server-side tracking moves the measurement off the browser, so leads and purchases that the pixel silently drops are recovered and reported.

First-party data, served from your own domain

TrakSolv hosts your Google Tag Manager server container on a subdomain of your Framer site. Requests come from a first-party context — they survive 3rd-party cookie deprecation, last far longer than client-side cookies, and look like your own traffic, not a tracker.

Clean, deduplicated conversions to every ad platform

The Framer dataLayer fires every GA4 enhanced-ecommerce event with a stable event_id, so your server-side container can forward leads and purchases to GA4, Meta CAPI, Google Ads Enhanced Conversions, TikTok and more — deduplicated against the browser pixel, with higher match quality.

Step by step

How to set up server-side tracking on Framer

Paste the TrakSolv loader and the Framer dataLayer snippet into your store's custom-code area, save, and you're done. Every step below has the exact snippet with a copy button.

Install guide

How to install on Framer

This is your install. Every step below has its exact snippet with a Copy button — go step by step. The Loader card above and DataLayer card below are reference copies of the same snippets.

  1. 1

    Open Site Settings → Custom Code

    Navigate

    Framer Editor → top-left Framer logo → Site Settings → General → Custom Code

  2. 2

    Loader — head half

    Loader • Head

    HEAD section → paste the Loader Head snippet

  3. 3

    Loader — body half + DataLayer (HEAD section, stacked)

    Loader • Body

    HEAD section → append the Loader Body snippet after the Head

    Framer applies HEAD code to every page; we put both halves there since Framer's BODY-end injection is per-page only.

  4. 4

    DataLayer snippet

    DataLayer

    HEAD section → append the DataLayer snippet right after the Body

    The DataLayer subscribes to framer:click / framer:formsubmit / framer:pageview — set a Tracking ID on each Framer link / form / CTA you want surfaced as select_item / generate_lead.

    HTML
    <!-- Traksolv · Framer DataLayer — Site Settings → General → Custom Code → HEAD -->
    <script>
    (function (w, d) {
      w.dataLayer = w.dataLayer || [];
      var DEBUG = false;
      try {
        var q = new URLSearchParams(location.search);
        if (q.get('tcDebug') === '1') { DEBUG = true; try { localStorage.setItem('tcDebug', '1'); } catch (e) {} }
        else if (q.get('tcDebug') === '0') { DEBUG = false; try { localStorage.removeItem('tcDebug'); } catch (e) {} }
        else { try { if (localStorage.getItem('tcDebug') === '1') DEBUG = true; } catch (e) {} }
      } catch (e) {}
      w.tcDebug = DEBUG;
      if (DEBUG && w.console && w.console.log) {
        try { w.console.log('%c[Traksolv]%c dataLayer debug ON — every push will log here. Disable with ?tcDebug=0', 'color:#22c55e;font-weight:600;background:#0a1f0c;padding:2px 6px;border-radius:3px', ''); } catch (e) {}
      }
      function log() {
        if (!DEBUG || !w.console || !w.console.log) return;
        try { w.console.log.apply(w.console, ['[Traksolv]'].concat([].slice.call(arguments))); } catch (e) {}
      }
      function push(eventName, eventId, params) {
        try {
          if (params && params.ecommerce !== undefined) w.dataLayer.push({ ecommerce: null });
          var ev = { event: eventName, event_id: eventName + '.' + eventId };
          if (params) for (var k in params) if (Object.prototype.hasOwnProperty.call(params, k)) ev[k] = params[k];
          w.dataLayer.push(ev);
          log('push ' + eventName, ev);
        } catch (e) {}
      }
      function once(key) {
        try { if (sessionStorage.getItem(key)) return false; sessionStorage.setItem(key, '1'); return true; }
        catch (e) { return true; }
      }
    
      w.tcFirePurchase = function (o) {
        try {
          if (!o || !o.orderId) return;
          if (!once('tc_purchase_' + o.orderId)) return;
          push('purchase', String(o.orderId), {
            ecommerce: {
              transaction_id: String(o.orderId),
              value:    Number(o.value) || 0,
              currency: o.currency || 'USD',
              items:    Array.isArray(o.items) ? o.items : []
            }
          });
        } catch (e) {}
      };
    
      function pageType() {
        var path = (location.pathname || '').toLowerCase();
        if (/thank[-_]?you|order[-_]?(confirm|complete|success)|receipt|confirmation/i.test(path)) return 'thank-you';
        return 'other';
      }
      w.tcPageType = pageType;
    
      // ── Subscribe to Framer's documented custom events ──
      d.addEventListener('framer:pageview', function (e) {
        try {
          var detail = (e && e.detail) || {};
          push('tc_pageview', String(location.pathname) + '.' + Date.now(), {
            page_path:   location.pathname,
            page_title:  d.title,
            locale:      detail.framerLocale || undefined
          });
          // Framer is a SPA — re-check page-type on every nav and try
          // firing purchase if we just landed on a thank-you page.
          if (pageType() === 'thank-you') firePurchaseIfReady();
        } catch (e) {}
      });
    
      d.addEventListener('framer:click', function (e) {
        try {
          var detail = (e && e.detail) || {};
          if (!detail.trackingId) return;
          // Honour merchant-set window.tcCatalog[trackingId] = { item_id,
          // item_name, price, currency } to enrich select_item with proper
          // GA4 item data. Otherwise just fire a thin select_item with the
          // tracking ID as the item identifier.
          var cat = (w.tcCatalog && w.tcCatalog[detail.trackingId]) || null;
          push('select_item', String(detail.trackingId) + '.' + Date.now(), {
            ecommerce: {
              item_list_id:   location.pathname,
              item_list_name: d.title,
              items: [cat ? {
                item_id:    String(cat.item_id || detail.trackingId),
                item_name:  cat.item_name || detail.trackingId,
                price:      Number(cat.price) || 0,
                quantity:   1
              } : {
                item_id:    String(detail.trackingId),
                item_name:  detail.trackingId,
                quantity:   1
              }]
            },
            link_url: detail.href || undefined
          });
        } catch (e) {}
      });
    
      d.addEventListener('framer:formsubmit', function (e) {
        try {
          var detail = (e && e.detail) || {};
          if (!detail.trackingId) return;
          push('generate_lead', String(detail.trackingId) + '.' + Date.now(), {
            form_id: String(detail.trackingId)
          });
        } catch (e) {}
      });
    
      function firePurchaseIfReady() {
        try {
          // 1) Explicit override.
          if (w.tcOrder && w.tcOrder.orderId) { w.tcFirePurchase(w.tcOrder); return; }
          // 2) Stripe redirect — Stripe Checkout appends ?session_id=cs_...
          //    Honour it if the merchant has wired their backend to update
          //    a window.tcOrder from session_id, but otherwise just fire
          //    with the session_id as orderId and zero value (merchant
          //    should override).
          var u; try { u = new URL(location.href); } catch (e) { u = null; }
          var sid = u && (u.searchParams.get('session_id') || u.searchParams.get('checkout_session_id'));
          if (sid) {
            w.tcFirePurchase({ orderId: sid, value: 0, currency: 'USD', items: [] });
            return;
          }
          // 3) Poll briefly for window.tcOrder to be set late.
          var attempts = 0;
          function tryFire() {
            var o = w.tcOrder;
            if (o && o.orderId) { w.tcFirePurchase(o); return; }
            if (++attempts > 30) return;
            setTimeout(tryFire, 100);
          }
          tryFire();
        } catch (e) {}
      }
    
      function boot() {
        if (pageType() === 'thank-you') firePurchaseIfReady();
      }
      if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', boot);
      else boot();
    })(window, document);
    </script>
    <!-- End Traksolv Framer DataLayer -->
    
    <!-- Traksolv · DataLayer enrichment — customer lifecycle + remarketing + attribution -->
    <script>
    (function (w, d) {
      function readLT() {
        try {
          var raw = localStorage.getItem('tc_lifetime');
          var p = raw ? JSON.parse(raw) : null;
          return {
            orderCount:    (p && p.orderCount)    || 0,
            lifetimeValue: (p && p.lifetimeValue) || 0,
            firstOrderTs:  (p && p.firstOrderTs)  || null,
            lastOrderTs:   (p && p.lastOrderTs)   || null,
            lastOrderId:   (p && p.lastOrderId)   || null
          };
        } catch (e) {
          return { orderCount: 0, lifetimeValue: 0, firstOrderTs: null, lastOrderTs: null, lastOrderId: null };
        }
      }
      function writeLT(lt) {
        try { localStorage.setItem('tc_lifetime', JSON.stringify(lt)); } catch (e) {}
      }
      function tier(v) {
        if (v >= 1000) return 'vip';
        if (v >= 250)  return 'mid';
        if (v > 0)     return 'low';
        return 'guest';
      }
      function readCookie(n) {
        try {
          var m = d.cookie.match(new RegExp('(^|; )' + n + '=([^;]+)'));
          return m ? decodeURIComponent(m[2]) : null;
        } catch (e) { return null; }
      }
      function readAttr() {
        try { var r = readCookie('_tc_attr'); return r ? JSON.parse(r) : null; } catch (e) { return null; }
      }
    
      w.tcReadLifetime    = readLT;
      w.tcCustomerTier    = function (v) { return tier(v != null ? v : readLT().lifetimeValue); };
      w.tcReadAttribution = readAttr;
    
      // ── Wrap tcFirePurchase to add customer-lifecycle + attribution ──
      // before the underlying snippet pushes the purchase event. Idempotent
      // per orderId, so customer refreshing the thank-you page doesn't
      // inflate their lifetime counters.
      var orig = w.tcFirePurchase;
      if (typeof orig === 'function') {
        w.tcFirePurchase = function (o) {
          try {
            if (!o || !o.orderId) return orig.apply(this, arguments);
            var lt = readLT();
            var firstFire = lt.lastOrderId !== String(o.orderId);
            var orderCount    = firstFire ? lt.orderCount + 1 : lt.orderCount;
            var lifetimeValue = firstFire ? lt.lifetimeValue + (o.value || 0) : lt.lifetimeValue;
            var firstOrderTs  = lt.firstOrderTs || Date.now();
            var newCustomer   = orderCount === 1;
            var daysSinceFirst = Math.floor((Date.now() - firstOrderTs) / 86400000);
    
            if (firstFire) {
              writeLT({
                orderCount:    orderCount,
                lifetimeValue: lifetimeValue,
                firstOrderTs:  firstOrderTs,
                lastOrderTs:   Date.now(),
                lastOrderId:   String(o.orderId)
              });
            }
    
            var attr = readAttr();
            // Build the enriched order. Preserve every original field; add
            // customer + attribution on top.
            var enriched = {};
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) enriched[k] = o[k];
            enriched.new_customer            = newCustomer;
            enriched.customer_type           = newCustomer ? 'new' : 'returning';
            enriched.customer_status         = 'customer';
            enriched.customer_lifetime_value = Math.round(lifetimeValue * 100) / 100;
            enriched.customer_order_count    = orderCount;
            enriched.customer_aov            = orderCount > 0 ? Math.round((lifetimeValue / orderCount) * 100) / 100 : 0;
            enriched.purchase_number         = orderCount;
            enriched.days_since_first_order  = daysSinceFirst;
            enriched.value_tier              = tier(lifetimeValue);
            if (attr) enriched.attribution = {
              landing:   attr.u   || null,
              referrer:  attr.r   || null,
              utm:       attr.utm || null,
              click_ids: attr.cid || null
            };
            return orig.call(this, enriched);
          } catch (e) {
            try { return orig.apply(this, arguments); } catch (e2) {}
          }
        };
      }
    
      // ── tc_visitor_context — emits visitor lifecycle + remarketing on
      //    every page-view. GTM / GA4 audience triggers + Google Ads
      //    dynamic remarketing tag read these.
      function visitorContext() {
        try {
          var lt = readLT();
          var attr = readAttr();
          var pageType = (typeof w.tcPageType === 'function') ? (w.tcPageType() || 'other') : 'other';
          w.dataLayer = w.dataLayer || [];
          w.dataLayer.push({
            event:                   'tc_visitor_context',
            event_id:                'tc_visitor_context.' + Date.now() + '.' + Math.random().toString(36).slice(2, 6),
            customer_status:         lt.orderCount > 0 ? 'returning_customer' : 'guest',
            customer_type:           lt.orderCount > 0 ? 'returning' : 'guest',
            value_tier:              tier(lt.lifetimeValue),
            customer_lifetime_value: Math.round(lt.lifetimeValue * 100) / 100,
            customer_order_count:    lt.orderCount,
            days_since_first_order:  lt.firstOrderTs ? Math.floor((Date.now() - lt.firstOrderTs) / 86400000) : null,
            ecomm_pagetype:          pageType,
            attribution: attr ? {
              landing:   attr.u   || null,
              referrer:  attr.r   || null,
              utm:       attr.utm || null,
              click_ids: attr.cid || null
            } : null
          });
        } catch (e) {}
      }
      if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', visitorContext);
      else visitorContext();
    })(window, document);
    </script>
    <!-- End Traksolv enrichment -->
  5. 5

    Wire purchase on the thank-you page (CRITICAL for purchase events)

    DataLayer

    Thank-you page → Page Settings → Custom Code → HEAD (or BODY end)

    Framer has no native order object. If you redirect from Stripe Checkout, the snippet auto-fires purchase with the session_id at value=0 — fine for Meta CAPI dedup but you'll want real revenue. Paste this block on the thank-you page with the order's actual value. Optional: enrich item_id / item_name from your product CMS.

    HTML
    <script>
      window.tcOrder = {
        orderId: new URLSearchParams(location.search).get('session_id')
                 || ('order-' + Date.now()),
        value:    97,            /* the order's actual value */
        currency: "USD",
        items: [{
          item_id:   "your-product-id",
          item_name: "Your Product Name",
          price:     97,
          quantity:  1
        }]
      };
    </script>
  6. 6

    Optional — set Tracking IDs on your CTA links

    Navigate

    Framer Editor → select a link/CTA → Link panel → Tracking ID field

    Any link with a Tracking ID fires framer:click → select_item with that ID. For richer GA4 items, populate window.tcCatalog before the snippet runs: window.tcCatalog = { 'buy-pro-plan': { item_id: 'plan_pro', item_name: 'Pro Plan', price: 49 } };

  7. 7

    Publish

    Save

    Save → Publish (top-right)

The loader Head + Body snippets are generated for your container inside the TrakSolv dashboard. Create a free account to grab them, or see the full how-it-works walkthrough.

FAQ

Server-side tracking for Framer, answered

Does server-side tracking work with Framer?

Yes. TrakSolv works with Framer via a lightweight loader snippet plus a Framer-specific dataLayer that fires every GA4 enhanced-ecommerce event. You paste it once in Framer's custom-code area and conversions start flowing through your own server-side GTM container.

How do I install TrakSolv on Framer?

Add the TrakSolv loader (head + body) and the Framer dataLayer snippet to your store's custom-code settings, then save. The full step-by-step is on this page. Most Framer merchants are live in a few minutes — no developer required.

What can I track server-side on Framer?

Page views, product views, leads and purchases, begin-checkout and more — every standard GA4 enhanced-ecommerce event, forwarded server-side to GA4, Meta CAPI, Google Ads and other destinations with deduplication.

How much does server-side tracking for Framer cost?

TrakSolv uses flat monthly pricing — one event sent to many destinations still counts as one event, and paid plans never auto-pause at quota. You can start free and go live in minutes.

Recover your Framer conversions — start free.

Spin up a first-party server-side GTM container for Framer and watch your match quality climb. No credit card, live in minutes.

WhatsApp
Server-Side Tracking for Framer — TrakSolv | Traksolv