Capturing and displaying dynamic content based on URL parameters transforms static Webflow landing pages into personalized experiences that match your marketing campaigns perfectly.
While traditional approaches require complex backend systems or expensive third-party tools, brix-utm attribute automatic content replacement seamlessly extracts values from UTM parameters and other URL data to personalize your Webflow pages instantly.
This approach enables dynamic headlines, personalized offers, and targeted messaging that matches your PPC campaigns, email marketing, and A/B tests without any server-side code.
Best of all, this Webflow attribute can be implemented for free in 5 minutes (or less!).
Understanding the strategic value of URL-based personalization helps you implement this feature effectively across different marketing channels:
URL parameters are the key to dynamic content without databases or server-side processing. When someone clicks your ad or email link, the URL can carry information that instantly personalizes their Webflow experience.
For example, this URL: yoursite.com/offer?utm_source=google&utm_medium=cpc&utm_term=premium+webflow+templates&location=NYC
Contains four parameters that can instantly customize the page:
Before implementing the technical solution, plan your parameter strategy for maximum impact:
UTM parameters are the industry standard for campaign tracking:
Beyond UTM parameters, create custom parameters for deeper personalization:
You can create as many custom parameters as you need, making them as specific as your campaigns require - there's no limit to how granular your personalization can be.
The setup involves adding JavaScript to your Webflow project and configuring elements with the brix-utm attribute.
Here's how to add the URL parameter detection functionality:
1 - Copy the implementation script:
/**
* BRIX Templates URL Parameter Auto-Population
* Automatically fills content in your Webflow site with data from URL parameters
* @version 2.1.0
*/
(function() {
'use strict';
// Configuration
const ALLOWED_PROTOCOLS = {
link: ['http:', 'https:', 'mailto:', 'tel:', 'sms:'],
image: ['http:', 'https:'],
default: ['http:', 'https:']
};
// Optional: Add your domains here to restrict external URLs (only applies to http/https)
const ALLOWED_DOMAINS = []; // e.g., ['example.com', 'www.example.com']
// Cache for URLSearchParams (performance optimization)
let urlParamsCache = null;
// Helper function to get URL parameters
function getUrlParameter(name) {
if (!urlParamsCache) {
urlParamsCache = new URLSearchParams(window.location.search);
}
const value = urlParamsCache.get(name);
// Treat whitespace-only as empty
return value && value.trim() ? value.trim() : null;
}
// Helper function to validate URLs for security
function isValidUrl(url, context = 'default') {
if (!url || !url.trim()) return false;
try {
const parsed = new URL(url, window.location.href);
// Get appropriate protocol list for context
const allowedProtocols = ALLOWED_PROTOCOLS[context] || ALLOWED_PROTOCOLS.default;
// Check protocol
if (!allowedProtocols.includes(parsed.protocol)) {
console.warn(`Blocked URL with disallowed protocol for ${context}:`, parsed.protocol);
return false;
}
// Only check domains for http/https URLs
if (ALLOWED_DOMAINS.length > 0 && (parsed.protocol === 'http:' || parsed.protocol === 'https:')) {
const hostname = parsed.hostname.toLowerCase();
const isAllowed = ALLOWED_DOMAINS.some(domain => {
const normalizedDomain = domain.toLowerCase();
// Exact match or subdomain match
return hostname === normalizedDomain ||
hostname.endsWith('.' + normalizedDomain);
});
if (!isAllowed) {
console.warn('Blocked URL with disallowed domain:', hostname);
return false;
}
}
return true;
} catch (e) {
return false;
}
}
// Helper function to format parameter values
function formatParameterValue(value, format) {
// Preserve "0" and other falsy-but-valid values
if (value == null || value === '') return '';
// Trim whitespace before formatting
value = String(value).trim();
// Apply formatting
switch(format) {
case 'uppercase':
return value.toUpperCase();
case 'lowercase':
return value.toLowerCase();
case 'capitalize':
return value.charAt(0).toUpperCase() + value.slice(1).toLowerCase();
case 'title':
// Note: This is ASCII-centric (English-oriented)
// For full Unicode support, you'd need \p{L} with 'u' flag
return value.toLowerCase().replace(/\b\w/g, l => l.toUpperCase());
default:
return value;
}
}
function initBrixUtm() {
// Reset cache for new run
urlParamsCache = null;
// Find all elements with brix-utm attribute
const elements = document.querySelectorAll('[brix-utm]');
if (elements.length === 0) {
return;
}
// Process each element
elements.forEach(element => {
const paramName = element.getAttribute('brix-utm');
// Skip if attribute is empty
if (!paramName) {
console.warn('Empty brix-utm attribute found, skipping element');
return;
}
const format = element.getAttribute('brix-utm-format');
const fallback = element.getAttribute('brix-utm-fallback');
// Get parameter value from URL (already trimmed)
const rawValue = getUrlParameter(paramName);
// Determine what value to use
let finalValue = null;
if (rawValue !== null) {
// We have a non-empty parameter value
finalValue = rawValue;
} else if (fallback) {
// No parameter or empty parameter, use fallback
finalValue = fallback;
}
// Apply formatting to whatever value we're using (param or fallback)
if (finalValue !== null && format) {
finalValue = formatParameterValue(finalValue, format);
}
// Only update if we have a value to set
if (finalValue !== null) {
// Check if already populated with this value (for idempotence)
const previousValue = element.getAttribute('data-brix-value');
if (previousValue === finalValue) {
return; // Already set to this value, skip
}
// Handle different element types
const tagName = element.tagName.toLowerCase();
let updateSuccess = false;
if (tagName === 'input' || tagName === 'textarea') {
// Form fields
if (element.value !== finalValue) {
element.value = finalValue;
// Fire events in correct order: input then change
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
}
updateSuccess = true;
} else if (tagName === 'select') {
// Select elements - verify the value actually took
const oldValue = element.value;
element.value = finalValue;
// Check if the value was actually set
if (element.value === finalValue) {
if (oldValue !== finalValue) {
// Fire events only if value changed
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
}
updateSuccess = true;
} else {
console.warn(`Select element could not accept value "${finalValue}"`);
}
} else if (tagName === 'img') {
// Images - only update if valid URL for image context
if (isValidUrl(finalValue, 'image')) {
element.src = finalValue;
// Don't auto-set alt to URL - that's poor accessibility
updateSuccess = true;
} else {
console.warn('Skipping invalid image URL:', finalValue);
}
} else if (tagName === 'a') {
// Links - only update href if explicitly requested and valid
if (element.getAttribute('brix-utm-target') === 'href') {
if (isValidUrl(finalValue, 'link')) {
element.href = finalValue;
updateSuccess = true;
} else {
console.warn('Skipping invalid link URL:', finalValue);
}
} else {
// Update text content
element.textContent = finalValue;
updateSuccess = true;
}
} else {
// All other elements - update text content
element.textContent = finalValue;
updateSuccess = true;
}
// Only mark as populated if update was successful
if (updateSuccess) {
element.classList.add('brix-populated');
element.setAttribute('data-brix-value', finalValue);
}
}
// If no value and no fallback, leave original content unchanged
});
// Clear cache after processing
urlParamsCache = null;
}
// Always initialize Webflow array (fixes brittle hook issue)
window.Webflow = window.Webflow || [];
// Run when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initBrixUtm);
} else {
initBrixUtm();
}
// Re-run on Webflow page changes (for AJAX navigation)
window.Webflow.push(function() {
initBrixUtm();
});
})();
2 - Add the script to Webflow:
3 - Publish your site: Click Publish in the top-right corner > Select your domains and publish to see changes live
Now let's set up your elements to automatically receive URL parameter data:
The brix-utm system uses simple, intuitive attributes:
Core attribute:
Formatting options for brix-utm-format:
Fallback handling:
You're running Google Ads campaigns targeting different cities. Your ads mention specific locations to increase relevance.
Webflow setup:
Element: H1 Heading
Attributes:
- brix-utm: location
- brix-utm-fallback: Your City
Content: Find a Webflow Agency in [Your City]
Result: Visitors from NYC see "Find a Webflow Agency in NYC", London visitors see their city, and direct visitors see "Your City"
You're sending email campaigns and want to personalize the landing page with the recipient's name.
Webflow setup for personalized greeting:
Element: H2 Heading
Element: H2 Heading
Attributes:
- brix-utm: name
- brix-utm-format: capitalize
- brix-utm-fallback: there
Content: Welcome back, [there]!
Result: John sees "Welcome back, John!" and Sarah sees “Welcome back, Sarah!”
Test different value propositions using URL parameters:
Webflow setup:
Element: Hero Heading
Attributes:
- brix-utm: headline
- brix-utm-fallback: Build Better Websites
Track conversions by variant in Google Analytics or your tracking platform of preference.
Ensure your personalization works flawlessly across all campaigns:
Here's how to fix the most common problems:
Content doesn't update with URL parameters:
Formatting doesn't apply:
Elements change content with a delay:
While this implementation focuses primarily on text content, it also supports:
For security, the script includes:
Add brix-utm="parameter-name" as a custom attribute to any heading element in Webflow Designer. When your URL contains ?parameter-name=value, that heading automatically displays the value. Add brix-utm-fallback="default text" for when no parameter exists.
Yes! The script works on any Webflow page including CMS template pages. You can combine CMS content with URL parameter personalization for powerful dynamic experiences on collection pages.
There's no technical limit, but for best performance stick to 10-15 dynamic elements per page. Focus on high-impact personalizations like headlines, offers, and CTAs for maximum conversion impact.
Yes! For images, apply brix-utm to an img element to change its source. For links, add brix-utm-target="href" to an a element to update the destination URL dynamically.
Implementing dynamic content with the brix-utm attribute transforms your Webflow landing pages into intelligent, personalized experiences that adapt to each visitor's journey and campaign source. This solution dramatically improves conversion rates through perfect message match, enables sophisticated campaign tracking without complex tools, and delivers the personalization modern users expect.
Start implementing URL-based personalization today to improve your ads quality scores, increase conversion rates, and deliver experiences that turn paid traffic into customers through intelligent message matching.
For advanced implementations requiring complex conditional logic, multi-step personalization flows, or integration with any marketing automation platforms, our Webflow development agency can help you with your specific marketing requirements.
Step-by-step guide to run any JavaScript function from Webflow buttons using the brix-button-js attribute, with params and async.
Control which bots access your Framer site with meta tags. Reduce bandwidth while keeping SEO strong and AI visibility intact.
Learn how to block AI bots and crawlers in Webflow with simple methods to save bandwidth and control which bots access your content.