Wix
Wix form fields are React-controlled and Wix won't let you set a field's Name, so values written straight to the DOM get reverted. The fix: match fields by their title and add a small helper script that hides them before the page paints and writes through React's value setter.
Add the tag to your site
In Settings → Custom Code, add the loader to Head, loaded on all pages, then publish.
<script src="https://attr.boggsmtk.com/v1/tag.js?k=YOUR_LICENSE_KEY"></script>
Swap YOUR_LICENSE_KEYfor the client's key from the dashboard.
Title each field to match the field map
In the Wix form editor, set each field's Field Title to exactly the Field Name it should hold — e.g. First Source or Session Count. The helper script matches on the title, so it must match character-for-character (the HTML Name, e.g. mtk_first_source, works as a title too). Make the fields not required.
Leave every MTK field set to Show.The helper script hides them from visitors — that's its job. Setting a field to Hidden in the Wix form editor removes it from the page entirely, so the script can't fill it and it submits empty. If attribution columns ever go blank, a well-meaning "Hidden" toggle on these fields is the first thing to check.
Add the companion Head script
Back in Settings → Custom Code, add the script below to Head, on all pages. It hides every titled MTK field before the browser's first paint(a stylesheet plus a MutationObserver, so React re-renders can't un-hide them and the fields never flash visible), then reads the mtk_attributiondataLayer event and writes each value through React's setter so the form state accepts it. Nothing to configure: the standard Field Names are built in so the hiding applies before first paint, and anything beyond them is picked up from the event, so a field added to the map later works without touching this script.
<!-- MTK Attribution Wix Companion Script -->
<script>
(function () {
// Values to keep OUT. Both of these are long enough to crowd a URL past
// its ceiling on their own, and everything inside them already rides
// along in the individual fields. Add any field you don't want on the
// platform — each one you drop frees room for the rest.
var EXCLUDE = [
'mtk_journey_json',
'mtk_attribution_summary'
];
// Only when the platform's key has to differ from the field name.
var RENAME = {
// 'mtk_first_channel': 'first_channel'
};
// The tag also publishes each value under its retired spelling so older
// client forms keep filling. That is for forms, not for us — carrying both
// would double the payload for nothing.
var LEGACY_PREFIXES = ['mtk_ft_', 'mtk_lt_'];
var LEGACY_NAMES = [
'mtk_time_since_paid',
'mtk_conversion_timestamp',
'mtk_summary',
'mtk_touch_path',
'mtk_touch_path_json'
];
// Titles the generic rule can't produce (click IDs stay lowercase).
var TITLE_EXCEPTIONS = {"mtk_gclid":"gclid","mtk_gbraid":"gbraid","mtk_wbraid":"wbraid","mtk_msclkid":"msclkid","mtk_page_url":"Page URL","mtk_fbclid":"fbclid","mtk_fbc":"fbc","mtk_fbp":"fbp"};
function isLegacy(key) {
for (var i = 0; i < LEGACY_PREFIXES.length; i++) {
if (key.indexOf(LEGACY_PREFIXES[i]) === 0) return true;
}
return LEGACY_NAMES.indexOf(key) > -1;
}
// 'mtk_first_channel' -> 'First Channel', matching the Field Name the
// dashboard shows for that row.
function titleOf(key) {
if (TITLE_EXCEPTIONS[key]) return TITLE_EXCEPTIONS[key];
var words = key.replace(/^mtk_/, '').split('_');
var out = [];
for (var i = 0; i < words.length; i++) {
if (!words[i]) continue;
out.push(
words[i] === 'json'
? 'JSON'
: words[i].charAt(0).toUpperCase() + words[i].slice(1)
);
}
return out.join(' ');
}
// Order values are added in, which on a length-capped platform is also
// drop priority: what a lead is worth least without goes last. Anything
// not listed (a custom field, a new default) sorts after these, so a drop
// is predictable rather than whatever order the event happened to be in.
var PRIORITY = [
'mtk_last_channel', 'mtk_last_source', 'mtk_last_medium', 'mtk_last_campaign',
'mtk_gclid', 'mtk_gbraid', 'mtk_wbraid', 'mtk_msclkid',
'mtk_first_channel', 'mtk_first_source', 'mtk_first_medium', 'mtk_first_campaign',
'mtk_page_path', 'mtk_page_url',
'mtk_conversion_unix', 'mtk_conversion_datetime', 'mtk_custom_conversion_time',
'mtk_fbclid', 'mtk_fbc', 'mtk_fbp',
'mtk_last_landing_page', 'mtk_last_landing_page_group', 'mtk_first_landing_page',
'mtk_session_count', 'mtk_pageview_count', 'mtk_paid_touch_count',
'mtk_last_paid_touch', 'mtk_time_since_last_paid_touch', 'mtk_time_to_conversion',
'mtk_journey_string'
];
// Every mtk_* value the event carries, minus the excluded and the retired.
function keysFrom(data) {
var out = [];
for (var key in data) {
if (!data.hasOwnProperty(key)) continue;
if (key.indexOf('mtk_') !== 0) continue;
if (isLegacy(key)) continue;
if (EXCLUDE.indexOf(key) > -1) continue;
out.push(key);
}
out.sort(function (a, b) {
var ia = PRIORITY.indexOf(a), ib = PRIORITY.indexOf(b);
if (ia === -1 && ib === -1) return a < b ? -1 : a > b ? 1 : 0;
if (ia === -1) return 1;
if (ib === -1) return -1;
return ia - ib;
});
return out;
}
// A field answers to its HTML name or its Field Name. Every spelling of
// the standard fields is seeded here — readable title, raw name, and the
// retired name the engine still publishes — so the hiding CSS applies
// before first paint; waiting for the tag's values would let a titled
// field flash visible. The map is then extended with whatever the event
// actually carries, so a custom field works without editing this script.
var LABEL_TO_KEY = {};
var SEEDED_TITLES = ["First Ad Placement","mtk_first_ad_placement","mtk_ft_placement","mtk_ft_ad_placement","Last Ad Placement","mtk_last_ad_placement","mtk_lt_placement","mtk_lt_ad_placement","First Campaign","mtk_first_campaign","mtk_ft_campaign","Last Campaign","mtk_last_campaign","mtk_lt_campaign","First Channel","mtk_first_channel","mtk_ft_channel","Last Channel","mtk_last_channel","mtk_lt_channel","First Content","mtk_first_content","mtk_ft_content","Last Content","mtk_last_content","mtk_lt_content","First Landing Page","mtk_first_landing_page","mtk_ft_landing","mtk_ft_landing_page","Last Landing Page","mtk_last_landing_page","mtk_lt_landing","mtk_lt_landing_page","First Landing Page Group","mtk_first_landing_page_group","mtk_ft_landing_page_group","Last Landing Page Group","mtk_last_landing_page_group","mtk_lt_landing_page_group","First Medium","mtk_first_medium","mtk_ft_medium","Last Medium","mtk_last_medium","mtk_lt_medium","First Query","mtk_first_query","mtk_ft_landing_query","mtk_ft_query","Last Query","mtk_last_query","mtk_lt_landing_query","mtk_lt_query","First Referrer","mtk_first_referrer","mtk_ft_referrer","Last Referrer","mtk_last_referrer","mtk_lt_referrer","First Source","mtk_first_source","mtk_ft_source","Last Source","mtk_last_source","mtk_lt_source","First Term","mtk_first_term","mtk_ft_term","Last Term","mtk_last_term","mtk_lt_term","gclid","mtk_gclid","mtk_lt_gclid","gbraid","mtk_gbraid","mtk_lt_gbraid","wbraid","mtk_wbraid","mtk_lt_wbraid","msclkid","mtk_msclkid","mtk_lt_msclkid","Journey String","mtk_journey_string","mtk_touch_path","Journey JSON","mtk_journey_json","mtk_touch_path_json","Session Count","mtk_session_count","Pageview Count","mtk_pageview_count","Paid Touch Count","mtk_paid_touch_count","Last Paid Touch","mtk_last_paid_touch","mtk_lt_paid_touch","Time Since Last Paid Touch","mtk_time_since_last_paid_touch","mtk_time_since_paid","Page Path","mtk_page_path","Page URL","mtk_page_url","Time To Conversion","mtk_time_to_conversion","Conversion Unix","mtk_conversion_unix","mtk_conversion_timestamp","Conversion Datetime","mtk_conversion_datetime","Custom Conversion Time","mtk_custom_conversion_time","Tracking Health","mtk_tracking_health","fbclid","mtk_fbclid","fbc","mtk_fbc","fbp","mtk_fbp","Attribution Summary","mtk_attribution_summary","mtk_summary"];
for (var si = 0; si < SEEDED_TITLES.length; si++) {
LABEL_TO_KEY[SEEDED_TITLES[si]] = SEEDED_TITLES[si];
}
function learn(data) {
// Matching a form field is not the same job as choosing a payload.
// keysFrom() drops every retired spelling on purpose — carrying both
// would double a length-capped URL — but the tag publishes those
// spellings precisely so older forms keep filling, and a field titled
// 'mtk_ft_source' is exactly that form. Match on the raw event keys, so
// a legacy-titled field is both filled and hidden.
for (var k in data) {
if (!data.hasOwnProperty(k)) continue;
if (k.indexOf('mtk_') !== 0) continue;
LABEL_TO_KEY[k] = k;
}
// keysFrom() still decides which fields get a readable title, so the
// titles stay the current ones rather than 'Ft Source'.
var keys = keysFrom(data);
for (var i = 0; i < keys.length; i++) {
LABEL_TO_KEY[keys[i]] = keys[i];
LABEL_TO_KEY[RENAME[keys[i]] || titleOf(keys[i])] = keys[i];
}
}
var style = document.createElement('style');
style.textContent =
'input[data-mtk-hide], textarea[data-mtk-hide], [data-mtk-hidden] { display: none !important; }';
(document.head || document.documentElement).appendChild(style);
function labelOf(el) {
var l = el.id && document.querySelector('label[for="' + el.id + '"]');
return ((l && l.textContent) || el.getAttribute('aria-label') || '').trim();
}
function wrapperOf(el) {
var node = el;
while (node.parentElement) {
var p = node.parentElement;
if (p.tagName === 'FORM' || p.querySelectorAll('input, textarea').length > 1) break;
node = p;
}
return node;
}
function ensureHidden(el) {
if (!el.hasAttribute('data-mtk-hide')) el.setAttribute('data-mtk-hide', '1');
var w = wrapperOf(el);
if (w !== el && !w.hasAttribute('data-mtk-hidden')) w.setAttribute('data-mtk-hidden', '1');
}
function sweep(root) {
if (!root || !root.querySelectorAll) return;
var els = root.querySelectorAll('input, textarea');
for (var i = 0; i < els.length; i++) {
if (LABEL_TO_KEY[labelOf(els[i])]) ensureHidden(els[i]);
}
}
new MutationObserver(function (muts) {
for (var i = 0; i < muts.length; i++) {
var m = muts[i];
if (m.type === 'childList') {
for (var j = 0; j < m.addedNodes.length; j++) {
if (m.addedNodes[j].nodeType === 1) sweep(m.addedNodes[j]);
}
} else if (m.target && m.target.nodeType === 1) {
sweep(m.target.parentElement || m.target);
}
}
}).observe(document.documentElement, {
childList: true, subtree: true,
attributes: true, attributeFilter: ['style', 'class', 'aria-label']
});
sweep(document);
function setReactValue(el, value) {
var proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement : HTMLInputElement;
Object.getOwnPropertyDescriptor(proto.prototype, 'value').set.call(el, value);
if (el._valueTracker) el._valueTracker.setValue(value === '' ? '\u0000' : '');
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
}
(function tick() {
var data = (window.dataLayer || []).find(function (e) { return e.event === 'mtk_attribution'; });
var settled = !!data;
if (data) learn(data); // pick up fields beyond the standard set
document.querySelectorAll('form input, form textarea').forEach(function (el) {
var key = LABEL_TO_KEY[labelOf(el)];
if (!key) return;
ensureHidden(el);
if (data && data[key] != null) {
if (el.value !== String(data[key])) setReactValue(el, String(data[key]));
} else {
settled = false;
}
});
setTimeout(tick, settled ? 2000 : 150);
})();
})();
</script>
<!-- End MTK Attribution Wix Companion Script -->Head placement matters: only code inline in the page's <head> runs before first paint. In Body-End (or loaded via GTM) the same script works, but the fields flash visible for a moment before hiding.