If you’re running HubSpot forms on your website, you probably want to know which page a lead was on before they submitted a form. HubSpot’s built-in tracking covers a lot, but populating a hidden field with the exact referring page URL gives your sales team immediate context — right inside the contact record.
Here’s a lightweight JavaScript approach that works reliably even with HubSpot’s CTA tracking redirects, React-based sites, and dynamic form rendering.
The Problem with Click-Based Tracking
The obvious approach is to listen for clicks on links to your form pages and store the current URL in sessionStorage. The problem is that HubSpot CTA buttons route through a redirect URL (e.g. /cs/c/?cta_guid=...) before landing on the form page. By the time the form loads, the stored value can be unreliable or missing entirely.
A More Reliable Approach
Instead of capturing the URL on click, store it on every page load — as long as the current page is not a form page. That way, the last non-form page the user visited is always sitting in sessionStorage, ready to be picked up when the form loads.
On the form page, a MutationObserver watches for HubSpot to render the form fields and sets the hidden field value as soon as it appears. A submit listener then re-applies the value at the last moment before the form sends — which is necessary on React-based sites where the framework can wipe input values during re-renders.
(function () {
const FORM_PATHS = ['/your-form-page', '/your-contact-page'];
const currentPath = window.location.pathname;
const isFormPage = FORM_PATHS.some(path => currentPath.includes(path));
// On every non-form page, store the current URL
if (!isFormPage) {
sessionStorage.setItem('internal_referral_page', window.location.href);
}
if (isFormPage) {
const ref = sessionStorage.getItem('internal_referral_page') || document.referrer;
if (ref) {
// Use the native setter to bypass React's synthetic event system
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set;
const setField = () => {
const input = document.querySelector("input[name*='internal_referral_page']");
if (input && input.value !== ref) {
nativeInputValueSetter.call(input, ref);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}
};
// Watch for HubSpot to render the form and set the value immediately
const observer = new MutationObserver(setField);
observer.observe(document.body, { childList: true, subtree: true });
// Re-apply the value right before the form submits in case React wiped it
document.addEventListener('submit', function() {
setField();
}, true);
}
}
})();
Setup Steps
1. Create the hidden field in HubSpot. In your HubSpot form editor, add a single-line text field and set its internal name to internal_referral_page. Mark it as hidden so it doesn’t show up for the user.
2. Update the form paths. Replace /your-form-page and /your-contact-page in the FORM_PATHS array with the actual URL paths where your forms live.
3. Add the script to your global JavaScript. Paste the snippet into your site’s main JS file, a custom code block in your CMS, or a Google Tag Manager custom HTML tag that fires on all pages. It needs to run sitewide — not just on the form pages.
Why the Native Input Setter?
On React-based sites, setting input.value = ref directly doesn’t work reliably. React controls form inputs through its own synthetic event system and will override values it doesn’t know about. Using the native HTMLInputElement prototype setter bypasses React’s control and forces the value in at a lower level, which React then picks up via the dispatched input and change events. Without this, the field appears populated in the console but submits empty.
Why the Submit Listener?
Even after setting the value on form render, React can wipe it again during a re-render triggered by the user interacting with other fields. Adding a submit listener in the capture phase (the true third argument) ensures the value is re-applied at the last possible moment before HubSpot sends the form data — after all user interaction but before the submission fires.
Why sessionStorage?
sessionStorage is scoped to the browser tab and cleared when the tab closes — which makes it a good fit here. You want the most recent page the user visited in that session, not something stored from a visit days ago. If a user opens the form directly in a new tab with no prior navigation, the script falls back to document.referrer, which captures the external source if one exists.
A Note on HubSpot Field Names
HubSpot sometimes prefixes hidden field names with an object type identifier — for example 0-1/internal_referral_page instead of just internal_referral_page. The script uses a contains selector (name*=) rather than an exact match to handle this automatically, so it will keep working even if HubSpot changes the prefix in the future.
How to Verify It’s Working
Open your browser’s DevTools console and navigate to any interior page, then click through to your form page. Run this in the console to confirm the field has been populated before you submit:
console.log(document.querySelector("input[name*='internal_referral_page']").value);
If that logs the referral URL, do a test submission and check the HubSpot contact record to confirm the value comes through on the backend.