A visitor arrives through a campaign link, browses two service pages, and submits your contact form.
The enquiry reaches your CRM. The campaign information does not.
The sales team sees a new lead, but nobody can confidently explain which campaign generated it.
A common implementation gap causes this: the website reads UTM parameters only on the page where the form is submitted. By that point, the visitor has moved away from the original tagged URL.
This article walks through a small, framework-independent pattern for preserving campaign information during same-tab, same-origin navigation.
The code is illustrative. Adapt it to your form implementation and test it before deployment.
1. Define what you want to preserve
Consider this campaign URL:
https://example.com/crm-development
?utm_source=linkedin
&utm_medium=paid_social
&utm_campaign=crm_demo
&utm_content=workflow_ad
The parameters describe different campaign attributes:
| Parameter | Example | Purpose |
|---|---|---|
utm_source |
linkedin |
Identifies the traffic source |
utm_medium |
paid_social |
Identifies the marketing medium |
utm_campaign |
crm_demo |
Identifies the campaign |
utm_content |
workflow_ad |
Distinguishes creative or link variations |
utm_term |
crm_integration |
Carries a keyword or targeting label when used |
Use a shared naming convention. Otherwise, values such as LinkedIn, linkedin, and linked-in can make reporting unnecessarily fragmented.
Do not put names, email addresses, phone numbers, or enquiry text into campaign parameters.
2. Choose an attribution rule before writing code
“Save the UTMs” leaves an important question unanswered: what happens when another tagged visit occurs?
For this example, we will use this rule:
Preserve the first tagged campaign encountered during the current browser tab’s page session.
That means:
- A tagged landing page creates the stored record.
- An untagged internal page preserves it.
- A later tagged URL in that page session does not overwrite it.
- An untagged visit without a stored record remains unattributed.
This is a limited capture rule. It does not identify a person’s first-ever interaction, and it does not reproduce GA4 attribution.
If your business needs the latest tagged campaign instead, implement that deliberately and name the CRM fields accordingly.
3. Capture campaign parameters on landing
Run the capture logic on every relevant landing page, before a visitor navigates away.
URLSearchParams handles reading the query string:
const UTM_KEYS = [
"utm_source",
"utm_medium",
"utm_campaign",
"utm_content",
"utm_term"
];
const STORAGE_KEY = "lead_campaign_v1";
function readCampaign() {
try {
const raw = sessionStorage.getItem(STORAGE_KEY);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
}
function captureCampaign() {
const existing = readCampaign();
if (existing) {
return existing;
}
const params = new URLSearchParams(
window.location.search
);
const hasCampaign = UTM_KEYS.some(
(key) => Boolean(params.get(key)?.trim())
);
if (!hasCampaign) {
return null;
}
const campaign = {
captured_at: new Date().toISOString(),
landing_path: window.location.pathname
};
for (const key of UTM_KEYS) {
campaign[key] = (params.get(key) || "")
.trim()
.slice(0, 200);
}
try {
sessionStorage.setItem(
STORAGE_KEY,
JSON.stringify(campaign)
);
} catch {
// Continue without persistent campaign storage.
}
return campaign;
}
const campaignForPage = captureCampaign();
This stores selected fields rather than copying the entire URL, which may contain unrelated or sensitive query parameters.
The length limit is a basic payload constraint. It does not make the values trustworthy.
Also, this example assumes your landing paths contain no personal information. Use an allowlisted page identifier if that assumption does not hold.
4. Understand the storage boundary
sessionStorage is scoped to the origin and browser tab’s page session. It survives ordinary reloads but is cleared when that page session ends.
Consequently, this implementation does not provide:
- Persistent attribution across future visits.
- Automatic sharing between different subdomains.
- Reliable continuity across separate tabs.
- Automatic transfer into an externally hosted form.
For example, www.example.com and forms.example.com have different origins.
If your enquiry journey crosses those boundaries, design an explicit handoff. Do not assume the stored values will follow the visitor.
Run campaign storage through your site’s consent controls where required. If storage is unavailable, the form should still work.
5. Attach the campaign to the form request
If your form already submits using FormData, add campaign fields when constructing the request:
const form = document.querySelector("#contact-form");
function buildLeadPayload(form) {
const payload = new FormData(form);
const campaign =
readCampaign() || campaignForPage;
for (const key of UTM_KEYS) {
payload.set(key, campaign?.[key] || "");
}
payload.set(
"campaign_landing_path",
campaign?.landing_path || ""
);
payload.set(
"campaign_captured_at",
campaign?.captured_at || ""
);
return payload;
}
// Inside your existing submission handler:
const payload = buildLeadPayload(form);
// Send payload using your existing request logic.
Keep this inside your existing submission flow. Adding another submit listener that independently sends the form can create duplicate enquiries.
For native HTML form submissions, populate named hidden inputs instead. The backend mapping is still necessary.
6. Validate and map the fields on the backend
Sending a field does not guarantee that it reaches the CRM.
Check each stage:
| Stage | What to verify |
|---|---|
| Browser request | Campaign fields appear in the submitted payload |
| Backend | Only expected fields are accepted and validated |
| CRM integration | Each field maps to the intended CRM property |
| CRM record | Values are saved and visible in reporting |
Treat browser-supplied attribution as untrusted metadata. Visitors can edit query strings and submitted fields.
Apply server-side type and length checks, and escape values when displaying them. Never use campaign parameters to determine access permissions or other sensitive decisions.
For existing contacts, define an update rule too. A repeat enquiry should not accidentally erase the contact’s original acquisition source.
A useful design is to keep original acquisition fields on the contact and submission-specific campaign details on each enquiry or activity record.
7. Test the complete journey
Use distinct test campaign values so records are easy to identify.
| Test | Expected result |
|---|---|
| Tagged landing page → contact form | Campaign values reach the CRM |
| Tagged landing page → internal page → form | Original campaign remains |
| Untagged visit with no stored campaign | Campaign fields remain empty |
| Second tagged URL in the same page session | First stored campaign remains |
| Storage unavailable | Form still submits; persistence may be lost |
| Form hosted on another origin | Explicit handoff is needed |
Inspect the request, backend processing, and final CRM record.
A correct browser payload proves only the first stage worked.
Campaign capture is one piece of attribution
Preserving UTMs makes an enquiry easier to investigate. It does not guarantee that CRM reports will match analytics reports.
Analytics may use different attribution rules, session definitions, consent signals, and advertising integrations.
Keep your implementation’s meaning explicit:
These fields record the first tagged campaign captured in this tab’s page session.
That definition gives developers, marketers, and sales teams a shared understanding of what the data can explain.
Top comments (0)